JSON output
You will be able to parse commission’s --json output without guessing: the shape of
a task, a context bundle, a board, an event, and a refusal, and the rules that
say which of those fields you may depend on.
Every command that emits structured data supports --json. Under --json,
stdout carries exactly one pretty-printed JSON value and nothing else; warnings,
notices, and refusals go to stderr, also as JSON. A pipeline can read stdout
without filtering.
The compatibility promise for these shapes — what may be added, what may not be removed — is in compatibility.md. Field addition is not breaking; parse defensively.
The task
The unit everything else embeds. It is the database row, serialised as-is, which is why a few fields are typed the way SQLite stores them rather than the way you would design them fresh.
{
"id": "myapp-2",
"projectId": 1,
"parentId": null,
"title": "Webhook ingest endpoint",
"body": "",
"status": "open",
"priority": 2,
"severity": null,
"dueAt": null,
"rank": null,
"sprint": null,
"sprintState": null,
"gate": "shipped",
"assignee": null,
"autonomy": null,
"preferredActor": null,
"mayExecute": null,
"version": 1,
"touches": "[\"src/http\",\"src/db\"]",
"planKey": "ingest",
"externalSource": null,
"externalKey": null,
"createdAt": "2026-07-27T14:49:17.039Z",
"updatedAt": "2026-07-27T14:49:17.039Z",
"closedAt": null,
"claimedBy": null,
"claimExpiresAt": null,
"claimExpired": false
}Three things that surprise people:
touchesandmayExecuteare JSON-encoded strings, not arrays. They are stored as text and handed back unparsed.JSON.parse(task.touches)gives you the list;nulland"[]"both mean “no labels”.statusis one of five values —open,in_progress,in_review,done,cancelled. There is noblocked: blocked is derived from unmet dependencies at read time, and appears as a section of the board rather than as a value here.versionis an optimistic-concurrency counter, incremented on every mutation. Pass it back as--if-version(CLI) orexpectedVersion(HTTP) to refuse a write that would clobber a change you did not see.
claimedBy, claimExpiresAt, and claimExpired are joined onto the row by
list views; they are properties of the claim, not of the task.
The context bundle
What commission show --json and commission next --json return — everything a fresh
session needs in one read.
| key | type | what it is |
|---|---|---|
project | object | slug, name, vision |
task | object | the task, as above |
parent | object | null | the parent task, when this one has one |
criteria | array | { id, taskId, text, done, position } — done is 0/1 |
deps | array | tasks this one waits on, with their status |
dependents | array | tasks waiting on this one |
children | array | subtasks |
links | array | attached documents and URLs to read |
evidence | array | what has been attached so far |
bindings | array | external objects this task is (Jira issue, GitHub PRs) |
gateRequires | string[] | evidence kinds the gate still needs |
approvals | object | { required, given, outstanding, stale } |
openQuestions | array | escalations waiting on a human |
decisions | array | answered questions — the durable reasoning |
notes | array | { ts, actor, text }, oldest first |
recentEvents | array | { ts, actor, type, payload } |
context | object | null | guidance fingerprint and drift, or null if nothing is tracked |
nextCommands | string[] | the commands that make sense from here |
nextCommands is worth using rather than reimplementing. It already accounts
for the task’s status, its claim, and what its gate is still missing.
Under a budget
--budget <tokens> trims the bundle and says what it dropped:
{
"truncated": true,
"omitted": { "events": 12, "olderNotes": 5, "links": 2 },
"estimatedTokens": 3820,
"budget": 4000
}Sections are given up in one fixed order — events, olderNotes, links,
dependents, children, bindings, evidence, parentCriteria, deps,
criteria, body, vision — so the same bundle at the same budget always
degrades identically. Task identity and state are never dropped. The token count
is a deliberate estimate (four characters to a token), not a tokenizer: a
guardrail against unbounded growth, not an accounting system.
The board
{
"project": { "...": "..." },
"inProgress": [],
"inReview": [],
"ready": [],
"blocked": [],
"recentlyDone": [],
"stale": [],
"counts": { "...": "..." }
}Every array holds tasks. blocked entries carry the blockers that put them
there. Nothing in this object is stored — it is one query, computed on read,
which is why it cannot disagree with the graph.
The ready frontier
commission ready --json returns { entries, batches }.
Each entry is { task, claimed, claimExpired, touchesOverlap, eligible, routing }:
touchesOverlap— ids of other ready tasks sharing atoucheslabel.eligible—{ eligible, reason, rule };ruleisautonomy,mayExecute, ornull.routing— why this actor would or would not get it, includingwhy(one sentence) andwouldChange(levers, each with the command that pulls it).
batches is a list of id lists with no touches overlap inside a batch —
a suggested fan-out. It is a heuristic over labels you wrote, not an analysis of
your code.
Events
commission log --json returns { events, nextCursor }:
{
"events": [
{
"id": 4,
"ts": "2026-07-27T14:55:22.303Z",
"occurredAt": null,
"actor": "blaze",
"projectId": 1,
"taskId": "myapp-1",
"type": "transition",
"payload": { "from": "open", "to": "in_progress" }
}
],
"nextCursor": 4
}payload is already parsed. Store nextCursor and pass it back as
--after; do not compare timestamps. Ids are a monotonic integer and the log
is append-only, so a cursor is exact where a timestamp is a race.
Event type values you will see: project, created, edit, criteria,
dep, note, evidence, claim, release, transition, approval,
question, answer, link, bind, unbind, sync, workspace.
The inbox
commission inbox --json — what needs a human.
{
"project": { "slug": "myapp", "name": "myapp" },
"actor": "blaze",
"scope": "actor",
"summary": "1 thing needs your judgment in myapp — 1 escalated decision",
"items": [],
"total": 0,
"counts": {
"approval": 0, "verification": 0, "decision": 0, "review": 0,
"blocked_on_human": 0, "divergence": 0, "stale_claim": 0,
"drift": 0, "delegation": 0
},
"inFlight": [],
"ready": 0
}counts enumerates every item kind, always, including the zeroes — a consumer
never has to distinguish absent from empty. Each item carries a rendered line
alongside its structured fields, so text and JSON cannot describe the same
situation differently.
Refusals
Refusals are on stderr, in one shape, on every surface. They are documented once in exit codes and the refusal shape.
Rules that hold everywhere
- stdout is the answer; stderr is everything else. Always.
- Empty is emitted, not omitted. A list with nothing in it is
[], not a missing key.envKeys,warnings,chain, andcountsfollow this deliberately. - Names are never values. The workspace record reports
envKeys(names) andenvFiles(paths); a secret value cannot reach--json, the event log, or the persisted record, because it never leaves the module that collected it. - Text output may change between minor versions. JSON shapes may not — see compatibility.md.
Related
- Exit codes and the refusal shape
- CLI reference — which commands take
--json - HTTP API — the same shapes over the wire
Part of Reference.
This page is docs/reference/json.md in the Commission
repository, rendered in place — the site keeps no copy of it. The repository is private, so there is no edit link to follow.