Status Cards
A status card is a standing question about a company's work, and the living answer to it. You give the card one message in plain language — what to watch, and how the update should read — and Paperclip's Summarizer compiles that into structured company-search queries, then keeps writing a Markdown summary as the matching issues move.
The API mirrors that lifecycle. You create a card and Paperclip queues a compile task; you ask for refreshes and it queues update tasks; the summarizer agent writes the compiled query and the finished summary back through two dedicated endpoints that only it may call.
Status cards are company-scoped, and company access is enforced on every request. If you are new to the API, read the API Overview first for base URL, authentication, and error-code conventions — this page builds on those and won't repeat them.
Experimental — off by default. Status cards are gated behind an instance experimental setting. Until an operator turns on
enableStatusCards, every endpoint on this page responds404 Not Foundwith{ "error": "Status cards are not enabled" }. Because the feature is still experimental, treat the request and response shapes below as subject to change between releases.
For the board itself — creating cards in the UI, reading the tiles, and what the update policy does — see Status Cards in the experimental section.
Enabling status cards
Every route here starts by asking the instance whether status cards are on. The server reads the instance's experimental settings, and if enableStatusCards is not true it stops immediately with:
{ "error": "Status cards are not enabled" }
returned as 404 Not Found. It is an instance-wide switch, not a per-company one, and it gates the API as well as the UI.
You also need the built-in Summarizer agent provisioned and ready for the company, unless the card names its own agent. When neither is available, requests that would start a run fail with 422 Unprocessable Entity, { "error": "Summarizer built-in agent is not configured" }, and a details object carrying code: "summarizer_not_configured" plus the built-in's current status. See Built-in Agents.
Who can call what
Three levels of access show up across these routes:
| Level | What it means |
|---|---|
| Company access | The caller can see the company. Enough for the read routes. |
tasks:assign |
The caller may assign work to agents. Required to create a card or to manage one, because both queue agent tasks. Board users without it get 403 Forbidden with the authorization explanation. |
| Summarizer only | Only the card's designated agent — its agentId override, or the built-in Summarizer — may write, and only from the run of the generation task it was given. |
Agents get one extra restriction on top: an agent may only manage status cards it authored. Managing someone else's returns 403 Forbidden with { "error": "Agents can only manage status cards they authored" }. Reading is unrestricted within the company.
Two hard limits apply to agent callers, from STATUS_CARD_AGENT_MAX_CARDS and STATUS_CARD_AGENT_MAX_INTEREST_PROMPT_LENGTH:
| Limit | Value | Failure |
|---|---|---|
| Cards one agent may author in a company | 20 | 422 Unprocessable Entity, { "error": "Agents can author at most 20 status cards" }. Deleting a card frees a slot. |
interestPrompt length for agent callers |
4,000 characters | 422 Unprocessable Entity, { "error": "Agent-authored status card prompts cannot exceed 4000 characters" }. |
Board callers keep the general 20,000-character limit from the request schema.
A card that is out of reach — another company's, or one that does not exist — is always reported the same way: 404 Not Found with { "error": "Status card not found" }.
The card object
| Field | Meaning |
|---|---|
id, companyId |
Identity. |
createdByUserId, createdByAgentId |
Who authored the card. Exactly one is set. |
title |
The card's name. Auto-generated by the compiler unless pinned. |
titlePinned |
When true, a recompile leaves title alone. |
interestPrompt |
The one message that drives the card: what to watch and how the update should read. |
queries |
The compiled company-search queries. Empty until the first compile lands. |
queryVersion |
Increments on every compiled-query write. |
queryCompiledAt, queryCompiledByAgentId |
When the query was last compiled, and by whom. |
agentId |
Per-card summarizer override, or null for the built-in Summarizer. |
refreshPolicy |
The update policy — see Refresh policy. |
state |
compiling, active, error, paused_budget, or paused_hours. |
pendingChangeCount, lastChangeAt |
Changes seen since the last update, and when they started piling up. |
fingerprint, fingerprintAt |
The snapshot of watched issues that change detection diffs against. |
mentionedIssueIds |
Issues referenced in the latest summary that joined the watched set. |
documentId |
The document holding the summary text, or null before the first write. |
lastUpdateRunKind |
full or incremental for the last content update, or null after a configuration change. |
lastGeneratedAt, lastModel |
When the last summary was written, and the model the agent reported. |
generatingIssueId |
The generation task currently in flight, or null. |
failureReason |
Plain-language explanation when state is error, otherwise null. |
nextEvalAt |
When the scheduler next evaluates this card. null in manual mode and while archived. |
archivedAt, archivedByUserId, archivedByAgentId |
Archive bookkeeping. |
createdAt, updatedAt |
Timestamps. |
Read routes return cards hydrated with three extra fields the database does not store: summaryBody (the current Markdown), todayTokens and todayCostCents (this card's spend since midnight UTC), and watchedIssueCount (how many issues the compiled query matches right now — omitted if the count could not be computed).
Card states
| State | What it means |
|---|---|
compiling |
Just created, or its prompt changed. The agent is compiling the query and writing the first summary. |
active |
Working normally. Whether the board shows it as fresh, stale, or updating is derived from pendingChangeCount and generatingIssueId. |
error |
The last generation task reached done, cancelled, or blocked without writing a summary. failureReason says which. |
paused_budget |
The daily token cap was reached. Automatic updates are suspended; a manual refresh still runs. |
paused_hours |
Outside the card's active hours. Changes batch into one update when the window opens. |
Refresh policy
refreshPolicy is an object on both create and update:
| Field | Required | Notes |
|---|---|---|
mode |
no | manual (default), interval, or reactive. |
intervalMinutes |
for interval |
Positive integer. Omitting it in interval mode fails validation with "Required for interval mode". |
debounceSeconds |
for reactive |
Positive integer. Omitting it in reactive mode fails validation with "Required for reactive mode". |
maxUpdatesPerHour |
no | Positive integer. Applies to reactive mode; defaults to 6 when unset. |
triggers |
no | Which changes count. See below. |
activeHours |
no | { start, end, timezone }, with start and end as HH:MM in 24-hour form and timezone a valid IANA identifier. Outside the window the card moves to paused_hours. |
dailyTokenCap |
no | Positive integer. When unset the engine applies a default of 100,000 tokens per UTC day before pausing to paused_budget. |
triggers decides what counts as a change worth an update:
| Trigger | Default | Fires on |
|---|---|---|
statusTransitions |
true |
A watched issue's status changed. |
membershipChanges |
true |
An issue started or stopped matching the query. |
humanComments |
true |
A person commented on a watched issue. |
assigneeChanges |
true |
A watched issue's assignee changed. |
anyUpdate |
false |
Any change at all, including in-progress churn. |
Active hours and the token cap only gate automatic work. A manual refresh or recompile request runs regardless.
List Status Cards
GET /api/companies/{companyId}/status-cards
Return the company's status cards, most recently updated first, each hydrated as described above. Any caller with access to the company may call it.
| Query parameter | Notes |
|---|---|
archived |
true returns archived cards, false (the default) returns live ones. |
Example
curl -sS \
-H "Authorization: Bearer {token}" \
"https://paperclip.example.com/api/companies/{companyId}/status-cards?archived=false"
Create a Status Card
POST /api/companies/{companyId}/status-cards
Create a card and immediately queue its compile task. Requires tasks:assign.
Request body:
| Field | Required | Notes |
|---|---|---|
interestPrompt |
yes | The card's standing request, trimmed, 1–20,000 characters. Agent callers are capped at 4,000. |
title |
no | 1–300 characters. Omit it and the compiler names the card. |
titlePinned |
no | Defaults to false. true protects the title from recompiles. |
agentId |
no | A per-card summarizer override, or null for the built-in Summarizer. Must belong to this company — otherwise 422 Unprocessable Entity, { "error": "Summarizer agent must belong to this company" }. |
refreshPolicy |
no | Defaults to { "mode": "manual" } with the default triggers. |
Status code. 201 Created, returning the card as it stands after the compile was queued — state: "compiling" with generatingIssueId set.
Creation and compilation are all-or-nothing: if the compile task cannot be created or the agent cannot be woken, the new card is deleted again and the underlying error is returned.
An agent caller must belong to the company it is creating in; if it does not, the response is 403 Forbidden with { "error": "Agent cannot author status cards for this company" }.
Example
curl -sS -X POST \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
"https://paperclip.example.com/api/companies/{companyId}/status-cards" \
-d '{
"interestPrompt": "Watch the launch project. Tell me what is blocked and the single next action.",
"refreshPolicy": {
"mode": "interval",
"intervalMinutes": 30,
"dailyTokenCap": 50000
}
}'
Get a Status Card
GET /api/status-cards/{id}
Return one hydrated card. Company access is enough.
Update a Status Card
PATCH /api/status-cards/{id}
Change a card's configuration, or archive and restore it. Requires tasks:assign, and agents may only patch cards they authored.
Request body — at least one field is required, otherwise the request fails validation with "At least one field is required":
| Field | Notes |
|---|---|
interestPrompt |
1–20,000 characters (4,000 for agents). Changing it queues a recompile and resets the card to compiling. |
title |
1–300 characters, or null to hand naming back to the compiler. |
titlePinned |
Whether recompiles may rename the card. |
agentId |
Summarizer override, or null. Must belong to this company. |
refreshPolicy |
The full policy object; it replaces the stored one and reschedules the card. |
archived |
true archives the card, false restores it. |
Two of these have side effects worth planning for:
- Editing
interestPromptqueues a compile task and clearslastUpdateRunKind, so the next content update is a full rebuild rather than a patch. The response is the card returned by that compile. - Archiving (
archived: true) clearsnextEvalAtandgeneratingIssueIdand cancels any in-flight generation task. Restoring (archived: false) queues a full refresh with triggerrestore, provided the card has compiled queries and nothing is already running.
Changing the agent also clears lastUpdateRunKind, for the same reason: a new writer should not patch text it did not write.
Delete a Status Card
DELETE /api/status-cards/{id}
Permanently delete the card and its update history. Requires tasks:assign, and agents may only delete cards they authored. Responds 204 No Content with an empty body.
Update History
GET /api/status-cards/{id}/updates
Return the card's update ledger, newest first. Company access is enough.
Each entry carries:
| Field | Meaning |
|---|---|
kind |
compile, full, or incremental. |
trigger |
manual, interval, reactive, or restore. |
status |
running, ok, or failed. |
generationIssueId, runId |
The task and agent run behind the update. |
changes |
The issue changes this update integrated, each with issueId, identifier, from, to, and changeKind. |
inputTokens, outputTokens, costCents, model |
What the update cost, attributed from the run's cost events. |
queryVersion |
The compiled-query version the update ran against. |
changeSummary |
The one-line note the agent wrote about this update. |
startedAt, finishedAt |
Timing. |
error |
Set when status is failed. |
changeKind is one of new, removed, status, assignee, human_comment, or updated.
Summary Revisions
GET /api/status-cards/{id}/summary-revisions
Return the card's past summaries, newest first, each with id, revisionNumber, title, body, changeSummary, and createdAt. A card that has never had a summary written returns an empty array. Company access is enough.
Recompile
POST /api/status-cards/{id}/recompile
Rebuild the card's compiled query from its current interestPrompt, then write a fresh full summary. Requires tasks:assign, and agents may only recompile cards they authored.
There is no request body.
Response.
| Field | Meaning |
|---|---|
card |
The card, now compiling, with generatingIssueId set. |
generatingIssue |
The compile task that was created or joined. |
alreadyGenerating |
true when a compile for this exact prompt was already genuinely in flight and this request joined it. |
Status codes. A newly queued compile responds 202 Accepted. Joining a compile that was already running responds 200 OK — so pressing the button twice does not queue two runs. A task that is blocked does not count as in flight: it is stuck waiting on a human, so a re-kick reopens it and counts as a fresh run.
Archived cards cannot be compiled: 422 Unprocessable Entity, { "error": "Archived status cards cannot be compiled" }.
Refresh
POST /api/status-cards/{id}/refresh
Ask the card to update now. Requires tasks:assign, and agents may only refresh cards they authored.
Request body:
| Field | Required | Notes |
|---|---|---|
full |
no | Defaults to false. true forces a full rebuild instead of letting Paperclip choose. |
A manual refresh always runs — it bypasses the card's active-hours window and daily token cap, and it runs even when change detection found nothing new.
Response.
| Field | Meaning |
|---|---|
card |
The card after the request. |
generatingIssue |
The update task, or null when nothing was queued. |
alreadyGenerating |
true when an update was already in flight and this request joined it. |
enqueued |
true when this request actually queued work. |
kind |
full or incremental, present when work was queued. |
changes |
The changes the update will integrate, present when work was queued. |
Status codes. 202 Accepted when enqueued is true and alreadyGenerating is false — that is, this request started the run. Everything else is 200 OK: joining a run already in flight, or a policy evaluation that decided to wait, pause, or do nothing.
Two refusals to plan for:
- A card whose query has not compiled yet:
409 Conflict,{ "error": "Compile the status-card query before refreshing it" }. - An archived card:
422 Unprocessable Entity,{ "error": "Archived status cards cannot be refreshed" }.
Example
curl -sS -X POST \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
"https://paperclip.example.com/api/status-cards/{id}/refresh" \
-d '{ "full": true }'
Dry Run
GET /api/status-cards/{id}/dry-run
Execute the card's compiled queries right now and return what they match, without writing anything or spending model tokens. This is what the board's Watched issues tab reads.
Unlike the other read routes, this one asks for an explicit company_scope:read decision — a caller confined to a narrower boundary gets 403 Forbidden with { "error": "Status-card dry-run is outside this actor's low-trust authorization boundary" }.
Response.
| Field | Meaning |
|---|---|
cardId |
The card. |
queryVersion |
The compiled-query version that was executed. |
queries |
One entry per compiled query, each with the query itself and its result. Queries are a union, so the same issue can appear under more than one. |
mentionedIssues |
Issue summaries for the issues the latest summary referenced, which are watched alongside the query matches. |
Write the Compiled Query
PUT /api/status-cards/{id}/query
This is the first of the two endpoints the summarizer agent calls during a compile. It is documented so you can understand what happens to the card — not because you will normally call it yourself.
Request body:
| Field | Required | Notes |
|---|---|---|
queries |
yes | 1–10 company-search queries. They are evaluated as a union. |
title |
yes | 1–300 characters. Ignored when the card's titlePinned is true. |
changeSummary |
yes | 1–2,000 characters, non-empty. |
generationIssueId |
yes | The compile task this write belongs to. |
A successful write stores the queries, increments queryVersion, stamps queryCompiledAt and queryCompiledByAgentId, keeps the card in compiling, and records a compile entry in the update ledger — plus a running entry for the full summary that is expected to follow in the same run.
Write the Summary
PUT /api/status-cards/{id}/summary
The endpoint the summarizer agent calls to land a finished summary, whether from a compile or from an update run.
Request body:
| Field | Required | Notes |
|---|---|---|
markdown |
yes | The summary body, 1–200,000 characters. Stored as a Markdown document. |
title |
no | 1–300 characters. |
changeSummary |
yes | 1–2,000 characters, non-empty. Attached to the new revision. |
generationIssueId |
yes | The generation task this write belongs to. |
model |
no | The model the agent actually used, up to 200 characters, or null. Stored on the card as lastModel. |
The rules both writes have to satisfy
Every one of these failures returns 403 Forbidden:
- The caller must be an agent, and must be either the card's
agentIdoverride or an agent carrying thesummarizerbuilt-in marker for this company — otherwise{ "error": "Only the card's summarizer agent may write status cards" }. Both stay eligible, so a task created before an agent switch can still land its result. - The write must name the task the card is currently waiting on —
{ "error": "Status-card write does not match the active generation task" }. - That task must be assigned to the calling agent —
{ "error": "Generation task is not assigned to this agent" }. - It must still be live —
{ "error": "Generation task is no longer active" }. - Its payload must target this card and company —
{ "error": "Generation task does not target this status card" }. - The request must come from that task's run —
{ "error": "Status-card write must run from the linked generation task" }.
Conflicts return 409 Conflict: { "error": "Status-card compilation was superseded by a newer task" } on the query write, and { "error": "Status-card generation was superseded by a newer task" } on the summary write.
Archived cards refuse both: 422 Unprocessable Entity, with { "error": "Archived status cards cannot accept generation writes" } for the query and { "error": "Archived status cards cannot accept summaries" } for the summary. A summary write also needs a compiled query first — 409 Conflict, { "error": "Compile the status-card query before writing its summary" }.
What a successful write does
The summary is appended as a new document revision rather than replacing the old text. The card then settles: state returns to active, generatingIssueId and failureReason are cleared, lastGeneratedAt / lastModel / lastUpdateRunKind are stamped, pending-change counters reset, and nextEvalAt is rescheduled from the refresh policy.
Two things happen to the watched set at the same time. Issues the Markdown references — by identifier such as ABC-123, or by an /issues/{uuid} link — are resolved against the card's company and stored in mentionedIssueIds, up to 200, so their later changes trigger updates too. Unknown identifiers and other companies' issues are dropped. And the fingerprint keeps the generation-time baseline for everything else, so an issue that changed while the summary was being written still shows up in the next diff instead of being silently absorbed.
The response contains the updated card, the document, and the new revision. The matching entry in the update ledger is closed out with status: "ok", the run's token and cost totals, and the model.
Response.
| Field | Meaning |
|---|---|
card |
The settled card. |
document |
The summary document, including latestBody and latestRevisionNumber. |
revision |
The revision that was just appended. |
Activity logging
Every mutating route records an entry on the company activity log against entityType: "status_card":
| Action | Recorded when |
|---|---|
status_card.created |
A card is created. Details include its initial state. |
status_card.updated |
A card is patched. Details include the changed fields and whether it is now archived. |
status_card.deleted |
A card is deleted. |
status_card.recompile_requested |
A compile is requested. Details include generatingIssueId and alreadyGenerating. |
status_card.refresh_requested |
A refresh is requested. Details include full, generatingIssueId, alreadyGenerating, and enqueued. |
status_card.query_written |
The agent writes a compiled query. Details include queryVersion, generationIssueId, and changeSummary. |
status_card.summary_written |
The agent writes a summary. Details include queryVersion, generationIssueId, documentId, and changeSummary. |
See the Activity page for how to read the log.
Where to go next
- Status Cards — the board itself, the update policy explained, and how to turn the feature on.
- Status Card Query skill — the bundled skill that teaches an agent to author cards and compile prompts into queries.
- Built-in Agents — how to provision the Summarizer, and what its lifecycle states mean.
- Summary Slots — the other Summarizer-written surface, fixed to a project or workspace header.
- Issues — compile and update tasks are ordinary issues.
- API Overview — base URL, authentication, company scoping, and the shared error-code table.