Agent / JSON Contract
Every command except the long-running dashboard supports --json, which makes stepbook scriptable by agents and CI. This page is the contract: the envelope shape, exit codes, and the per-command data payloads. For the narrative version, see Driving Stepbook from an Agent.
The envelope
With --json, a command prints exactly one JSON object to stdout:
{
"ok": true,
"command": "run",
"version": 1,
"data": {
"pipeline": "demo",
"ts": "2026-06-30T12:00:00.000Z",
"git_sha": "a1b2c3d",
"blob": "state/runs/2026-06-30T12-00-00-000Z.json",
"totals": { "pass": 8, "fail": 0 },
"root": { "unit": "demo", "children": [] }
}
}| Field | Type | Meaning |
|---|---|---|
ok | boolean | true on success, false on error. |
command | string | The command that produced this envelope. |
version | number | Envelope schema version. Currently 1. Guard against drift. |
data | object | Command-specific payload (present when ok is true). |
On error, the shape is:
{
"ok": false,
"error": {
"code": "ConfigError",
"message": "unknown step: anaylze"
}
}error.code mirrors the failure class: ConfigError for config/usage errors (exit 2, like the unknown-step case above) and CLIError for runtime failures (exit 1). See Exit codes below.
stdout is clean
In --json mode all human-readable info output is redirected to stderr, so stdout contains only the envelope. You can parse it directly without filtering.
Exit codes
The exit code mirrors ok, with two failure classes:
| Code | ok | Meaning |
|---|---|---|
0 | true | Success. |
1 | false | Runtime failure — a step threw, a check failed, output unreadable. |
2 | false | Config / usage error — bad flags, missing config, unknown step. |
An agent can branch on the exit code alone (e.g. if stepbook check --json; then …) without parsing the body. Use the body to find what failed.
The trajectory tree
run and check return the run as a trajectory tree under data.root, not a flat step list. Each node is a TrajectoryNode — the entrypoint runner is the root, and every unit it invoked (via a typed call or ctx.run) is a child, nested by call order:
{
"unit": "parse", // the invoked unit's name
"subkey": "parse#0", // "<unit>#<index>" — index among its siblings
"index": 0,
"input": { "text": "…" },
"output": { "wordCount": 12 },
"error": null, // { "message": string } when the unit threw
"meta": { "latencyMs": 3, "cacheHit": false },
"checks": [{ "name": "wordCount > 0", "pass": true }],
"cacheHit": false,
"children": [] // sub-calls this unit itself made
}run's data is { pipeline, ts, git_sha, blob, totals, root }; check's is { pipeline, ts, git_sha, totals, failed, root }. Repeated calls to one unit (e.g. agent turns) appear as sibling nodes unit#0, unit#1, ….
Branching on results
The fields agents reach for most:
# Did all checks pass? 0 = yes.
stepbook check --json | jq '.data.totals.fail'
# Which units had a failing check? Walk the trajectory tree.
stepbook check --json | jq '[.data.root | recurse(.children[])
| select(any(.checks[]; .pass == false)) | .unit]'
# Pull a value out of a step's output (structured YAML query).
# `.data.result` is the faithful query value — a scalar, array, or object,
# whatever the query produced (`.data.runs[].value` covers multi-run selectors).
stepbook q analyze "select: density" --json | jq '.data.result'The development loop
A representative agent loop — execute, assert, inspect:
stepbook run --json > /tmp/run.json
stepbook check --json | jq -e '.data.totals.fail == 0' # exits non-zero if any failed
stepbook q analyze "select: longWords" --jsonTo fix a regression, edit the step file and re-run from that step — the cache invalidates only the edited step (and downstream), because the cache key includes the source-file hash:
stepbook run --from analyze --json
stepbook check --jsonSee The Cache.
Output mode rules
--jsonand--mdare mutually exclusive — passing both is a usage error (exit2).--jsonoverrides the human-mode--formatflag onq.- In
--mdmode, the command writes a Markdown report tostate/reports/and prints its path to stdout (not the report body).
Versioning
version is currently 1 for every command. If a future release changes a payload shape in a breaking way, the version increments. Agents should assert the version they were written against and fail loudly on mismatch rather than parsing an unexpected shape.
MCP server
@stepbook/mcp ships an MCP server (stepbook-mcp bin) that exposes every CLI command as a tool over stdio. It's a thin shim over the same envelopes documented here — each tool spawns the CLI with --json and returns the envelope as the MCP tool's structuredContent. The shapes above are the shapes the tools return.
Tool naming maps verb-for-verb: stepbook_run, stepbook_check, stepbook_q, and so on. Eleven tools total. dashboard (long-running server, doesn't fit MCP's request/response shape) and the cases / save-case fixture commands are not exposed.
Errors come back as isError: true on the tool result, with the error envelope preserved as structuredContent — the same ok: false shape the CLI emits.
See the MCP Server guide for the catalog and per-tool argument shapes.