The Attention Queue
The attention queue exposes paused runs that need an answer or inspection, and provider
limit waits (a used-up usage limit, or a rate limit that outlasted its retries). A
provider-limit wait retries automatically at its deadline, the provider’s resetAt or the
computed backoff, and pauses again if the limit persists. A person, an agent,
or a workflow can list the items, follow them as a stream, and read one, all as JSON.
Branch on kind before acting: only attention and awaiting_response accept answers;
usage_limit needs no answer, and unreadable needs inspection. A single-run read returns
null when there is no queue item. Archon routes nothing: every answerer sees every item,
and a second answer to the same pause is refused.
The contract
Section titled “The contract”| Operation | CLI | HTTP |
|---|---|---|
| List | archon workflow attention --json [--project <id|name|key> | --all] [--label k=v]… [--sort age|label:<key>] | GET /api/runs/attention?project=…&label=…&sort=… |
| Count | archon workflow attention --json --count-by project|label:<key> | GET /api/runs/attention?countBy=… |
| Stream | archon workflow attention --watch --json [--project … | --all] [--label k=v]… [--after <cursor>] | Not yet |
| Read one | archon workflow attention <run-id> --json | GET /api/runs/<run-id>/attention |
| Label | archon workflow attention label <run-id> --occurrence <id> [--set k=v]… [--unset k]… | POST /api/runs/<run-id>/attention/labels |
| Answer | archon workflow answer <run-id> --occurrence <id> …, or respond, approve, reject | POST /api/workflows/runs/<run-id>/answer |
| Give up on the run | archon workflow abandon <run-id> | POST /api/workflows/runs/<run-id>/abandon |
The CLI reference lists every flag. The list, the
single read, the stream and workflow wait all derive from the same run projection, so
they cannot disagree about what a run needs.
- Scope. The CLI defaults to the project of the working directory.
--projectnames another project and--allcovers the install. Everyworkflow attentionform runs from any directory, registered or not. Over HTTP there is no working directory, so the default is the install. - Filters.
--label k=vis an exact match on the label’s value, and repeated filters must all match. No filter or field depends on who is asking. - Order.
ageputs the oldest pause first.label:<key>sorts by that label’s value as a plain string. The engine reads no meaning into labels or messages.
Queue items
Section titled “Queue items”{ "kind": "attention", "runId": "7c1e…", "workflow": "deliver", "project": { "key": "acme/web", "codebaseId": "c41f…", "name": "acme/web" }, "fanOut": null, "revision": 1, "labels": { "ci": "other" }, "pausedAt": "2026-10-14T07:58:02.000Z", "context": { "artifactsDir": "/home/me/.archon/workspaces/acme/web/artifacts/runs/7c1e…", "transcript": "/home/me/.archon/workspaces/acme/web/logs/7c1e….jsonl", "workingPath": "/home/me/.archon/workspaces/acme/web/worktrees/fix-ci", "branch": "fix-ci" }, "node": "base", "occurrence": "e0f4…", "message": "CI on windows failed twice with EPERM. Retry, or stop?", "form": "select", "options": [{ "id": "retry" }, { "id": "stop" }]}kindsays which action applies.attentionis await: attentionpause: answer it in itsformwithanswer, or therespond/approve/rejectshorthand.awaiting_responseis an approval gate: answer it withapprove,rejectorrespond.usage_limitis a run paused on a provider limit:nodehit the limit andresetAtis when the run tries again. It covers a used-up usage limit the provider reported a reset for, and a rate limit (HTTP 429) that was still refusing the node after its retries. For a usage limit,resetAtis the provider’s reset. For a rate limit, it is the provider’s reset when one was reported, otherwise a computed backoff of 5 to 15 minutes. The item needs no answer and continues by itself afterresetAt, starting from the node that hit the limit;archon workflow resume <run-id>tries sooner, and abandon stops it. If the limit is still there when the node runs again, the run pauses on a new wait. Withworkflows.autoResumeOnQuotaReset: false, or for a container run, the run fails instead of waiting. It has nooccurrence.unreadableis a paused run Archon cannot describe, or a parent whose sub-run tree it cannot read: a sub-run with no row, or a tree that revisits a run or exceeds 500 runs;reasonanddetailsay why. A container paused because this host cannot read its frozen source also appears asunreadable, with reasonsource_release_not_continued. Resume it manually on a host where the recorded frozen source is readable; containers do not continue automatically after a source release. For other unreadable items, inspect the run and resume or abandon it.occurrenceidentifies an answerable pause (attentionorawaiting_response). Pass it back with--occurrence; an answer or label write for a pause the run has left is refused, naming the current occurrence.revisionstarts at 1 and grows by one with every label write on an attention wait. It stays 1 for other kinds, so it does not identify every item change.projectis the project the run belongs to, never a label.keyis the project storage key that state documents scope by, soarchon state --project <item.project.key>reaches the same project.codebaseIdis null for a project with no codebase row, such as a run in an unregistered directory (_cwd/<name>).nameis the codebase’s name, or otherwise the key’s last segment. A run started before its codebase was renamed keeps the key it started with; that is the key its state lives under.projectis null only for a run with no trusted storage root.fanOutis{ parentRunId, nodeId, index }for a child of afan_out:node, and null otherwise. A parent blocked on its children is never an item; reading it returns the item of the child that holds the pause.pausedAtis when the pause opened. Gates do not record it, so it is null for them, and they sort after timed items.contextholds pointers, never pasted content. Read what you need.
Labels
Section titled “Labels”A wait: attention node can label its pause. Values may use runtime references; they are
resolved when the wait pauses.
- id: ask depends_on: [classify] wait: attention: "$classify.output.summary" form: select options: [{ id: retry }, { id: stop }] labels: ci: "$classify.output.cause" # e.g. "pr" or "other" area: deliveryLabels are a flat map from string to string. A key is one token with no = or
whitespace. Anything else that classifies an item, such as a handler, writes labels with
attention label. The write merges into the map: a key it sets replaces the same key, a
key it unsets is removed, and nothing else changes. Only attention waits carry labels; a
gate’s item always has none.
Counting shows which classes of question recur often enough to deserve a handler:
archon workflow attention --json --all --count-by label:ciStream the queue
Section titled “Stream the queue”--watch turns the list into a stream of JSON lines, one object per line, across every run
in scope, including runs that start after the stream does. It is the alternative to polling
the list and diffing it. It runs until Ctrl-C, SIGTERM, or its reader closes stdout, and
then exits 0. On Windows a process kill is not a signal: it ends the stream without that
exit.
archon workflow attention --watch --json --allA stream starts with one attention line per open item, with snapshot: true, then a
ready line. The lines before ready are the complete open set: on every connect, an item
you held that is not among them is gone. After ready, a line arrives for every change the
store commits, whichever process made it: a detached run, a trigger, the server, another
host on the same database.
type | When | Fields |
|---|---|---|
attention | An item opened or changed | cursor, snapshot, project, item (a queue item, as above) |
resolved | An item this stream sent closed or was replaced | cursor, project, runId, occurrence, reason; with reason: "answered", also answeredVia and answeredAt |
unmatched | A label write moved an open item outside --label | cursor, project, runId, occurrence, revision |
terminal | A run ended | cursor, project, runId, status, outcome |
resumed | A failed run resumed, an engine-cancelled fan-out child recovered, or a paused or stale-running run resumed with stored error evidence | cursor, project, runId, status: "running" |
owner_lost | A running run’s owner process is gone | cursor, project, runId, observedStatus |
ready | The snapshot is complete | cursor |
bookmark | 30 seconds passed with no other line, and the stream read the store up to cursor | cursor |
error | The store cannot read the stream’s cursor: --after named one this install did not issue, or the store no longer reads the stream’s own position. The last line; the command exits 4 | error: "cursor_invalid" |
- Keep an open set keyed by
runId. Eachattentionline replaces that run’s item, including when its kind or usage-limitresetAtchanges whilerevisionstaysresolvedremoves the run’s item. Itsoccurrenceidentifies an answerable pause; it is null forusage_limitandunreadable.reasonisanswered, orwithdrawnwhen the run resumed, ended, was deleted, or moved to a different pause. A usage-limit wait that resumes is withdrawn, with null occurrence.answeredAtis null when the row had moved past the answer before the stream read it.
- Delivery is at least once. Replacing the open item by run ID is idempotent; do not
dedupe all kinds by occurrence or revision, since usage-limit waits have no occurrence
and their reset time can change at revision 1. Use
(runId, occurrence, revision)only for answerable pauses. Dedupeterminalandresumedon(runId, cursor).owner_lostis a live observation: it is not replayed, and the next connection reports it again if the owner is still gone. - Resume where you stopped. Persist the
cursorof the last line you handled and restart with--after <cursor>. The new stream sends the snapshot andready, then replays every change after that cursor.bookmarklines keep the cursor moving while the queue is quiet. A stream that cannot read the store sends nobookmarkuntil a read succeeds, so no line for well over 30 seconds means the stream is not reading. If the cursor is rejected, rerun without--after. --labelfiltersattentionandresolved.terminal,resumed, andowner_lostarrive for every run in scope. A label write that leaves an open item outside the filter sendsunmatched, even for an item the stream never sent.- Queue items report the row when the stream read it. The stream reads about once
a second, and PostgreSQL wakes it on each commit. A pause opened and answered between
two reads may never appear as an item.
terminalandresumedreport committed transitions in feed order, even if the run has since moved on. A failed run that resumes emitsresumedwithstatus: "running"; the human output printsRun <id> resumed (running).Reconnecting with--afterthe failure’s cursor replays that resume, so a stream consumer can clear the stale failure. Aresumedline reports a transition to running, not a guaranteed prior terminal state: resuming a paused or stale-running run also emits it when a nonempty stored error is cleared. Ordinary paused-run resumes without that error do not emitresumed. - Owner liveness follows
workflow wait. A running run is watched until it pauses or ends, including while it reports stalled tool calls. Only a run whose recorded owner is on this host and user can be reportedowner_lost; a run owned on another host is never reported lost. Nothing here changes the run: abandon it withworkflow abandon.
The stream only reads. Stopping it, with a signal or by closing its stdout, affects no run
and exits 0. A closed stdout is noticed when the stream next writes a line. An answered run continues where a continuation host runs: archon serve,
archon workflow wake --watch, or a CLI answer without --json, which continues the run in
place.
Handle a class of questions with a workflow
Section titled “Handle a class of questions with a workflow”Labels are how your own handler workflows find their items. A workflow that hits a recurring class of question pauses with a label for that class. To have that class handled, add a handler workflow that selects it from the queue and answers it. Until a handler exists, the items wait for any answerer.
This handler answers delivery runs whose CI went red for a reason outside the change,
labelled ci=other. Start it from a schedule so it polls
the queue.
name: ci-other-handlerdescription: Retry or stop delivery runs paused on CI failures the change did not causenodes: - id: queue bash: archon workflow attention --json --all --label ci=other - id: decide depends_on: [queue] prompt: | Each item in this queue is a delivery run paused because CI failed for a reason outside the change. For every item, read its transcript (context.transcript) and decide whether retrying can succeed.
$queue.output output_format: type: object properties: answers: type: array items: type: object properties: runId: { type: string } occurrence: { type: string } option: { type: string, enum: [retry, stop] } required: [runId, occurrence, option] required: [answers] - id: answer depends_on: [decide] bash: | answers=$decide.output.answers items=$(printf '%s' "$answers" | jq -c '.[]') || exit 1 if [ -z "$items" ]; then exit 0 fi failed=0 while read -r item; do if ! result=$(archon workflow respond "$(jq -r .runId <<<"$item")" "$(jq -r .option <<<"$item")" \ --occurrence "$(jq -r .occurrence <<<"$item")" --json); then printf '%s\n' "$result" >&2 failed=1 continue fi if ! printf '%s\n' "$result" | jq -e '.ok == true' >/dev/null; then printf '%s\n' "$result" >&2 failed=1 fi done <<<"$items" exit "$failed"Answer commands exit non-zero when an answer is refused, including with --json
(ok: false). An accepted answer at full capacity exits 0: the run keeps its place
and a continuation host continues it when a slot frees. --json records the answer
without continuing inline; run archon workflow wake or use archon serve.
Two handlers can watch the same class. The occurrence check refuses the second answer, and the run stays answered once.
Classify, then route
Section titled “Classify, then route”A handler can also classify items for other handlers instead of answering them. This example puts a cheap classifier in front of the queue:
-
Watch. A scheduled workflow polls
archon workflow attention --json --all, or a long-running process readsarchon workflow attention --watch --json --all, and keeps thekind: attentionitems whoselabelshave noclassyet. -
Classify. For each item it reads the message, labels, project, form, options and
pausedAt, and asks a small model for a typed choice: a class, an urgency, and whether an agent can answer it, with a confidence. -
Label. It writes the result back without answering:
Terminal window archon workflow attention label <run-id> --occurrence <occurrence> \--set class=ci-other --set urgency=high --set answerable=agent --jsonThe item’s
revisiongoes up, and the classifier skips it next time because it now has aclass. When the classifier is confident of the answer itself, it can answer withrespondinstead. -
Route. A second handler selects its class, like the
ci=otherhandler above:Terminal window archon workflow attention --json --all --label class=ci-other --label answerable=agent
The labels are yours. Archon never reads class, urgency or answerable, and never
decides whether a person or an agent answers.
