DocsWorking with AIScripting with JSON
Back
Working with AI

Scripting with JSON

Every command an agent or script is likely to need speaks JSON. The payloads and results are the same shapes the MCP server uses, so anything you can do through a tool call you can do from a shell script, with the same validation.

Two kinds of --json

Input and output. These commands read a payload from stdin and print a JSON result:

CommandPayloadResult
xpo add --jsontitle (required), description, status, parent, story_points, priority, assignee, labels, cycle_id, links, and before or after{id, title, status}
xpo update <id> --jsonany of the add fields except a required title — at least one{id, messages}
xpo comment <id> --json{body} (and optionally id){id}
xpo link --json{source, target, type, overwrite}{source, target, kind, replaced}
xpo unlink --json{source, target, type}{source, target, kind}
xpo start --json{id, force}{id, branch, worktree_path, messages}
xpo merge --json{id, strategy, commit_message, keep_branch}{id, merge_sha, messages}
xpo spec --json / xpo walkthrough --json{operation, issue_id, content} — write, read, or delete{ok, issue_id, path, content}
xpo artifact --json{operation, issue_id, filename, content} — add, read, delete, or list{ok, issue_id, path, content, artifacts}

Payloads are strict: an unknown field is an error, so a typo fails loudly instead of being silently ignored. Stdin must be piped. merge's strategy is squash (default), merge, or ff — leave it out unless you need a specific one.

In an update payload, null clears assignee, cycle_id, priority, story_points and parent. On description, labels and links, null means "unchanged"; clear those with "" or []. before and after take an issue ID and place the issue directly above or below it in the manual order.

Output only. These print JSON instead of text:

CommandResult
xpo list --json{issues: [...]}
xpo show <id> --jsonthe issue, with dependencies, comments, events, children, and more. Add --with-spec / --with-walkthrough to inline them
xpo comments <id> --json{comments: [...]}
xpo history [id] --json{timeline: [...]}
xpo blocked --json{issues: [...]} — all BLOCKED issues
xpo inbox --json{items: [...]}
xpo rationale <query> --json{results, query, total_matches}
xpo done, planned, blocked <id> with --json{id, messages}
xpo spec <id> --json, xpo walkthrough <id> --json{ok, issue_id, path, content}
xpo pulse --jsonproject pulse metrics

Examples

Create an issue and capture its ID:

id=$(echo '{"title": "Fix token refresh", "labels": ["bug"], "priority": 1}' \
  | xpo add --json | jq -r .id)

Start it and cd into the worktree:

cd "$(echo "{\"id\": \"$id\"}" | xpo start --json | jq -r .worktree_path)"

List every blocked issue's ID and title:

xpo blocked --json | jq -r '.issues[] | "\(.id)\t\(.title)"'

Issue IDs

Anywhere an ID is accepted — arguments or payload fields — you can use the full ID (myapp-a1b2c3), the bare hash (a1b2c3), or any unique part of it. Ambiguous matches fail with issue ID '<id>' is ambiguous (matches N issues).

Errors

On failure, a --json command prints a single envelope to stdout and exits with status 1:

{
  "error": "cannot start issue myapp-a1b2c3: blocked by incomplete issue myapp-d4e5f6"
}

This applies to everything — validation errors, unknown issues, bad flags, and running outside an xpo project — so a script only has to check the exit code and read .error.

History format

xpo history --json prints one envelope rather than a line per event:

{
  "timeline": [
    {"kind": "issue_event", "timestamp": "…", "issue_id": "myapp-a1b2c3", "event_type": "UPDATE",
     "payload": {"status": "DOING"}, "created_by": "Claude Code <agent@mcp>", "on_behalf_of": "Alice <[email protected]>"},
    {"kind": "commit", "timestamp": "…", "sha": "…", "message": "…", "author": "…", "branch": "…"}
  ]
}

With an ID it shows that issue's events and its branch commits; without one, a project-wide timeline (--limit, default 50). Earlier versions printed JSON Lines — update any script that reads history --json line by line.