JSON Events & Structured Output
Parsing what Codex did and forcing schema-conforming results
- Explain why `--json` exists and how the JSONL event stream differs from default stdout, and capture it pipe-safely
- Identify the core event types (thread.started, turn.started, turn.completed, turn.failed, item.*, error) and what each carries
- Read the four token-usage fields on turn.completed and use them for cost and observability
- Distinguish the item types (agent_message, reasoning, command_execution, file_change, mcp_tool_call, web_search, plan/todo) and parse the ones you care about with jq
- Force a schema-conforming final response with `--output-schema` and persist it with `-o`, and do the same in the SDK via `run(input, { outputSchema })`
- Combine the event stream and structured output to drive logging, dashboards, and downstream automation steps
By default `codex exec` prints prose meant for humans, which is useless to a script. This lesson makes Codex output *programmable*: you'll walk the `--json` event stream (with its event types, item types, and token-usage fields), then force the final answer to conform to a JSON Schema with `--output-schema` (and the SDK's `outputSchema`). That pairing — a structured event log plus a validated result — is the bridge from one-off runs to reliable CI, dashboards, and SDK integrations.
- 1The mental model: two output channels, two jobs
- 2The event stream: the run as a timeline
- 3Token usage: the bill, on every turn
- 4Item types: the concrete things Codex did
- 5Forcing a shape: --output-schema and -o
- 6The same contract in the SDK
- 7Putting it together: a programmable run
The mental model: two output channels, two jobs
Start with the one idea that makes everything else click: codex exec deliberately splits its output into two channels, and --json swaps what flows down one of them.
When you run codex exec "…" non-interactively, Codex:
- streams its progress (what it's reading, running, thinking) to stderr, and
- prints only the final agent message to stdout.
That split is what makes it pipe-friendly. RESULT="$(codex exec 'summarize the repo')" captures just the answer, because the noisy progress went to stderr and never landed in your variable. The default stdout is prose for a human — perfect to read, useless to parse reliably.
--json changes the contract on stdout. Instead of one final prose message, stdout becomes a JSONL stream: one JSON object per line, one per event, as the run unfolds. Now stdout is structured data for a machine. You're no longer scraping text — you're consuming an event log you can filter, count, and route.
Think of it as two different deliverables from the same run:
Default codex exec | codex exec --json | |
|---|---|---|
| stdout carries | The final agent message (prose) | A JSONL event stream (one object per line) |
| Audience | A human reading the terminal | A program parsing each line |
| Best for | Quick answers, RESULT="$(…)" | Logging, dashboards, CI gating, downstream steps |
| Progress (stderr) | Streamed to stderr | Still streamed to stderr |
A note on the flag name: --json and --experimental-json are listed as aliases in the CLI reference. The naming is still settling, so confirm against codex exec --help on your installed version rather than assuming one is canonical.
Key insight
Why progress goes to stderr
Because progress streams to stderr and the result to stdout, RESULT="$(codex exec '…')" captures only the answer even without --json. With --json, stdout becomes the structured event log instead — but stderr keeps carrying human-readable progress, so you can still watch a run live while a parser consumes stdout.
Note
Verify the flag on your version
The CLI reference lists --json with the alias --experimental-json. Whether one is being renamed is unsettled — run codex exec --help on the version you have installed and trust that over any hard-coded assumption.
The event stream: the run as a timeline
With --json, every line on stdout is one event describing something that just happened. Read top to bottom, the stream is a timeline of the whole run. There are two layers: a handful of lifecycle events that bracket the run and its turns, and item events that wrap the concrete things Codex does inside a turn (covered in the next section).
The lifecycle events you'll see:
| Event | When it fires | Notable payload |
|---|---|---|
thread.started | The session/thread begins | thread_id — capture this to resume the thread later |
turn.started | A model turn begins | — |
turn.completed | A turn finishes successfully | a usage object (token counts — see below) |
turn.failed | A turn fails | error details |
item.started | An item begins | the item with status: "in_progress" |
item.updated | An item's state changes | the updated item |
item.completed | An item finishes | the completed item with results |
error | An error occurs | error details |
Two of these deserve special attention up front:
thread.startedcarries thethread_id. Stash it (e.g. to an env var or a file) and you can later runcodex exec resume <SESSION_ID> "<follow-up>"to continue exactly where you left off — the backbone of multi-step automations.turn.failedanderrorare your failure signals in the stream. The exactcodex execexit-code table isn't fully documented, so the robust rule is: treat a non-zero exit as failure, and additionally watch forturn.failed/errorevents if you want to react to why it failed, not just that it did.
A turn is one model round-trip; a complex task is many turns. So a single run typically looks like thread.started → (turn.started → many item.* → turn.completed) repeated → end. That nesting is the structure your parser walks.
Tip
Capture thread_id immediately
The very first useful thing to pull from the stream is thread_id from thread.started. Persist it before anything else — it's the only handle you have to resume the same conversation in a later step (codex exec resume <id>).
Watch out
Don't rely on an exit-code table that isn't published
The precise exit codes for codex exec are not fully documented. Code defensively: branch on non-zero exit for failure, and parse turn.failed/error events when you need the reason. Don't assume specific numeric codes.
Token usage: the bill, on every turn
Each turn.completed event carries a usage object. These four fields are how you turn an agent run into a line on a dashboard or a cost report:
| Field | What it counts |
|---|---|
input_tokens | Tokens sent into the model this turn (your prompt + context) |
cached_input_tokens | The subset of input tokens served from cache — typically billed cheaper |
output_tokens | Tokens the model generated as visible output |
reasoning_output_tokens | Tokens spent on internal reasoning (separate from visible output) |
Why each matters in practice:
cached_input_tokensis the lever behind prompt caching. A high cache ratio means you're re-sending the same context (yourAGENTS.md, repo files, instructions) cheaply across turns. If it's near zero on a long multi-turn run, your context is churning and your bill is higher than it needs to be.reasoning_output_tokensis its own bucket because reasoning effort is configurable (model_reasoning_effort). On ahigh/xhighrun this can dwarf the visible output — which is exactly the cost you're trading for better answers. Logging it separately lets you see what reasoning effort is actually costing you.- Summing
input_tokens+output_tokensacross everyturn.completedgives you the run's total token footprint — the raw number you multiply by the live rate card to estimate cost.
Because usage arrives per turn, a multi-turn task emits several turn.completed events; add them up to get the run total rather than reading only the last one.
Exact prices and the credit rate card change often, so don't hard-code a cost-per-token. Capture the token counts from the stream and apply the current rates from the live pricing page.
Watch out
Prices rot — token counts don't
The credit rate card and per-token USD prices change frequently. Log the four usage fields (they're stable), and convert to money against the live rate card at https://developers.openai.com/codex/pricing — never bake a price into your script.
Example
Sum tokens across a run with jq
Total all input+output tokens for a finished run:
codex exec --json "refactor the auth module" \
| jq -s '[.[] | select(.type=="turn.completed") | .usage]
| { input: map(.input_tokens) | add,
output: map(.output_tokens) | add,
reasoning: map(.reasoning_output_tokens) | add }'-s slurps the JSONL stream into an array first; the filter then sums each field across every completed turn.
Item types: the concrete things Codex did
Inside a turn, the interesting content rides on items — wrapped by item.started, item.updated, and item.completed events. The item.type field tells you what kind of thing it is, so you can parse only what you care about and ignore the rest.
The item types you'll encounter:
item.type | Represents | Useful fields |
|---|---|---|
agent_message | The assistant's text output | text |
reasoning | A reasoning/thinking step | — |
command_execution | A shell command Codex ran | command, status |
file_change | A file Codex created/edited/deleted | the change details |
mcp_tool_call | A call to an MCP server tool | tool/result details |
web_search | A web search Codex performed | query/results |
| plan / todo updates | The agent's plan or todo-list state | the plan items |
This taxonomy is what makes the stream genuinely useful. A few patterns fall straight out of it:
-
Extract the final answer. Filter for a completed
agent_messageitem and read itstext— the structured way to get the same string default stdout would have printed:bashcodex exec --json "summarize the changes" \ | jq -r 'select(.type=="item.completed" and .item.type=="agent_message") | .item.text' -
Audit every command it ran. Collect each
command_executionitem to build a log of exactly what touched your machine — gold for security review and CI audit trails. -
Show a live activity feed. Map
file_change,command_execution, andweb_searchitems into UI rows for a dashboard that shows what the agent is doing right now. -
Track progress. The plan/todo updates let you render a checklist that fills in as the agent works.
The jq filters above are an applied pattern, not a verbatim doc quote — the event and item shapes (the type field, item.type, the fields per item) are the documented contract; the exact filter you write on top is yours.
Tip
Filter by type, ignore the rest
Because every line is tagged with type and items carry item.type, you never have to understand the whole stream. Decide which item types your automation cares about (often just agent_message and command_execution) and let jq/your parser drop everything else.
Key insight
command_execution is your audit log
Every shell command Codex runs surfaces as a command_execution item with its command and status. Persisting these gives you a complete, replayable record of what an autonomous run did — exactly what you want when reviewing an agent that operated unattended in CI.
Forcing a shape: --output-schema and -o
The event stream tells you what Codex did. Often you also need the result itself in a guaranteed shape — not prose you have to coax a value out of. That's what --output-schema is for. It's the headless equivalent of "structured outputs": you hand Codex a JSON Schema file, and it forces the final response to conform to that schema.
codex exec --output-schema ./schema.json \
-o ./result.json \
"Audit this repo's dependencies and report status"Two flags working together:
--output-schema <path>points at a JSON Schema file. The final response must validate against it — so a downstream step canJSON.parsethe result and trust the fields exist, instead of regexing prose.-o <path>(alias of--output-last-message) persists the final message to a file in addition to printing it to stdout. Pair the two and you get a schema-validresult.jsonon disk, ready for the next CI step.
A minimal schema looks like this:
{
"type": "object",
"properties": {
"summary": { "type": "string" },
"status": { "type": "string", "enum": ["ok", "action_required"] },
"issues": { "type": "array", "items": { "type": "string" } }
},
"required": ["summary", "status"],
"additionalProperties": false
}Now a CI job can gate on status == "action_required" with total confidence the field is present. This is the difference between hoping the model said "ok" somewhere in a paragraph and programming against a contract.
You can combine --output-schema with --json too: the event stream gives you the full audit trail of the run, while the schema-validated final message gives you the clean result. Stream for observability; schema for the payload.
Tip
Schema for the payload, stream for the story
Use --output-schema -o result.json to get a validated result your next step can parse, and --json to get the event log of how the result was produced. They answer different questions — what's the answer? vs what did it do to get there? — and compose cleanly.
Example
Gate CI on a structured field
codex exec --output-schema ./schema.json -o ./result.json \
"Review the diff; report status and any blocking issues"
if [ "$(jq -r .status ./result.json)" = "action_required" ]; then
echo "Codex flagged blocking issues:"; jq -r '.issues[]' ./result.json
exit 1
fiNo prose parsing — the schema guarantees status and issues are there.
The same contract in the SDK
Everything above has a programmatic twin in the Codex SDK, so you don't have to shell out to the CLI from your application. The SDK wraps the same engine — in fact the TypeScript SDK literally spawns the codex CLI and exchanges these JSONL events over stdio — so the concepts map one-to-one.
Structured output is exposed via the outputSchema option on run():
import { Codex } from "@openai/codex-sdk";
const schema = {
type: "object",
properties: {
summary: { type: "string" },
status: { type: "string", enum: ["ok", "action_required"] },
},
required: ["summary", "status"],
additionalProperties: false,
} as const;
const codex = new Codex();
const thread = codex.startThread();
const turn = await thread.run("Summarize repository status", { outputSchema: schema });
console.log(turn.finalResponse); // JSON conforming to the schemaThe result lands on turn.finalResponse — the SDK's equivalent of the schema-validated stdout message. The Python SDK mirrors this shape: thread.run("…") returns a result whose final_response holds the answer.
To react to the stream of events (not just the final answer), the TS SDK offers thread.runStreamed(input), which yields an async generator of stream events — including "item.completed" (carrying the item) and "turn.completed" (carrying usage). These are the same event and item types you saw on the CLI's --json stream, just delivered as objects instead of JSONL lines. And thread.id is the SDK's thread_id — persist it and call codex.resumeThread(threadId) to continue later.
The payoff: whether you drive Codex from a shell script (--json + --output-schema) or from application code (runStreamed + outputSchema), you're consuming one contract — a typed event stream plus a schema-validated result.
Key insight
CLI and SDK are the same contract
--output-schema ⇄ run(input, { outputSchema }); the --json event stream ⇄ runStreamed's events; -o's final message ⇄ turn.finalResponse. Learn the event/item types and the schema mechanism once and both surfaces are covered.
Note
Versions move together
The SDK pins a CLI runtime, so @openai/codex-sdk and the codex CLI advance in lockstep. Read both current versions live (npm + GitHub Releases) rather than assuming a pairing.
Putting it together: a programmable run
Step back and see the whole pattern. A reliable automation around Codex has two halves, and you've now met both:
- Observe the run via the
--jsonevent stream — capturethread_id, tallyusagefor cost, auditcommand_executionandfile_changeitems, and surface progress to a dashboard. - Trust the result via
--output-schema(or SDKoutputSchema) — get a final payload your next step can parse without guessing.
That combination is what lets you wire Codex into things that can't tolerate free text:
- CI gates — fail the build when a schema field says so; attach the run's
usagetotals and command log as artifacts. - Dashboards — render a live feed of items as they complete, plus per-run token spend.
- Downstream steps — feed the schema-valid JSON straight into the next job (open a PR, post a Slack message, update a ticket) with no scraping.
- Multi-step jobs — persist
thread_idandresumeto continue a conversation across pipeline stages.
The through-line of this whole module: default output is for humans; --json and --output-schema are for programs. The moment you stop reading Codex's prose and start consuming its events and validated results, you've crossed from running a tool by hand to building automation on top of an agent.
A pipe-and-gate skeleton that ties it together:
# Capture the thread id from the stream, log usage, and persist a schema-valid result
codex exec --json --output-schema ./schema.json -o ./result.json \
"Run the test suite; report status and failing tests" \
| tee run.jsonl \
| jq -rc 'select(.type=="thread.started") | .thread_id' > thread.id
jq -r .status ./result.json # -> "ok" or "action_required", guaranteed presentTip
Observe with the stream, trust the schema
Reach for --json when you need to watch and audit a run (events, usage, commands), and --output-schema/outputSchema when you need to act on its result. Most production automations use both at once.
Try it: Make a Codex run programmable
Goal: turn a single codex exec run into a structured event log plus a schema-valid result you can gate on. 1) Pick a tiny task. In a small repo, choose something with a clear answer, e.g. "List the project's direct dependencies and whether each is pinned." 2) Stream it as JSON. Run codex exec --json "<task>" | tee run.jsonl and inspect the lines — find thread.started, the turn.started/turn.completed brackets, and a few item.* events. 3) Pull the answer with jq. Extract just the assistant text: jq -r 'select(.type=="item.completed" and .item.type=="agent_message") | .item.text' run.jsonl. 4) Tally usage. Sum the token fields across turns: jq -s '[.[] | select(.type=="turn.completed") | .usage] | { in: map(.input_tokens)|add, out: map(.output_tokens)|add, reasoning: map(.reasoning_output_tokens)|add }' run.jsonl. 5) Force a shape. Write a schema.json with summary (string) and status (enum ok/action_required) as required fields, then run codex exec --output-schema ./schema.json -o ./result.json "<task>". 6) Gate on it. Add a shell if [ "$(jq -r .status ./result.json)" = "action_required" ]; then exit 1; fi and confirm it branches without parsing any prose. 7) Stretch (SDK). Reproduce step 5 in the TypeScript SDK with thread.run("<task>", { outputSchema }) and read turn.finalResponse; capture thread.id and resume with a follow-up. 8) Write three sentences: which item types did you actually need, what did the usage totals tell you about cost, and how did gating on a schema field differ from scraping prose? You'll leave with the core instinct of this module — default output is for humans, --json and --output-schema are for programs.
Key takeaways
- 1`codex exec` splits output: progress to stderr, the final agent message to stdout. `--json` (alias `--experimental-json`) replaces stdout with a JSONL event stream — structured data for machines instead of prose for humans.
- 2Lifecycle events bracket the run: thread.started (carries thread_id — capture it to resume), turn.started, turn.completed, turn.failed, item.started/updated/completed, and error. Treat non-zero exit (and turn.failed/error) as failure.
- 3Each turn.completed carries a usage object — input_tokens, cached_input_tokens, output_tokens, reasoning_output_tokens — that you sum across turns for cost and observability; convert to money against the live rate card, never a hard-coded price.
- 4Items wrap the concrete actions: agent_message (text), reasoning, command_execution (command/status), file_change, mcp_tool_call, web_search, and plan/todo updates — filter by item.type to parse only what you need (e.g. agent_message for the answer, command_execution for an audit log).
- 5`--output-schema ./schema.json` forces the final response to conform to a JSON Schema; pair with `-o` to persist a validated result.json so downstream steps program against a contract instead of scraping prose.
- 6The SDK exposes the identical contract — `run(input, { outputSchema })` → `turn.finalResponse` for the result, `runStreamed` for the same event/item stream — so CLI and SDK automations share one model.
Quiz
Lock in what you learned
Check your understanding
0 / 4 answered
1.You run `codex exec --json "refactor the module"` in a CI pipeline and read stdout. What is on stdout, and where did the progress messages go?
2.Your automation needs to resume the same Codex conversation in a later pipeline step, and also report how many tokens the run cost. Which two pieces of the event stream do you capture?
3.You want to extract just the assistant's final text answer from a `--json` run, and separately build an audit log of every shell command Codex executed. Which item types do you filter for?
4.A CI job must fail the build whenever Codex's review finds a blocking issue, and you don't want to scrape prose to decide. What's the right approach?
Go deeper
Hand-picked sources to keep learning
The source of truth for `--json`, the event/item types, `--output-schema`, `-o`, stdin modes, and resuming exec sessions.
Full `codex exec` flag list including `--json`/`--experimental-json`, `--output-last-message`/`-o`, `--output-schema`, and `--sandbox`.
TypeScript and Python SDKs: `run(input, { outputSchema })`, `turn.finalResponse`, `runStreamed`, and thread resume.
Worked examples of `outputSchema`, the streamed event types (`item.completed`, `turn.completed`), and `resumeThread`.
Live token prices and credit rates — apply these to the `usage` token counts; never hard-code a cost.
End-to-end example of headless Codex in CI — the destination this lesson's structured output unlocks.