Skip to content

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:

json
{
  "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": [] }
  }
}
FieldTypeMeaning
okbooleantrue on success, false on error.
commandstringThe command that produced this envelope.
versionnumberEnvelope schema version. Currently 1. Guard against drift.
dataobjectCommand-specific payload (present when ok is true).

On error, the shape is:

json
{
  "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:

CodeokMeaning
0trueSuccess.
1falseRuntime failure — a step threw, a check failed, output unreadable.
2falseConfig / 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:

jsonc
{
  "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:

sh
# 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:

sh
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" --json

To 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:

sh
stepbook run --from analyze --json
stepbook check --json

See The Cache.

Output mode rules ​

  • --json and --md are mutually exclusive — passing both is a usage error (exit 2).
  • --json overrides the human-mode --format flag on q.
  • In --md mode, the command writes a Markdown report to state/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.

Released under the MIT License.