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.