Agentic AI AcademyAgentic AI Academy

JSON Events & Structured Output

Parsing what Codex did and forcing schema-conforming results

Advanced 12 minBuilder
What you'll be able to do
  • 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
At a glance

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.

  1. 1The mental model: two output channels, two jobs
  2. 2The event stream: the run as a timeline
  3. 3Token usage: the bill, on every turn
  4. 4Item types: the concrete things Codex did
  5. 5Forcing a shape: --output-schema and -o
  6. 6The same contract in the SDK
  7. 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 execcodex exec --json
stdout carriesThe final agent message (prose)A JSONL event stream (one object per line)
AudienceA human reading the terminalA program parsing each line
Best forQuick answers, RESULT="$(…)"Logging, dashboards, CI gating, downstream steps
Progress (stderr)Streamed to stderrStill 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:

EventWhen it firesNotable payload
thread.startedThe session/thread beginsthread_idcapture this to resume the thread later
turn.startedA model turn begins
turn.completedA turn finishes successfullya usage object (token counts — see below)
turn.failedA turn failserror details
item.startedAn item beginsthe item with status: "in_progress"
item.updatedAn item's state changesthe updated item
item.completedAn item finishesthe completed item with results
errorAn error occurserror details

Two of these deserve special attention up front:

  • thread.started carries the thread_id. Stash it (e.g. to an env var or a file) and you can later run codex exec resume <SESSION_ID> "<follow-up>" to continue exactly where you left off — the backbone of multi-step automations.
  • turn.failed and error are your failure signals in the stream. The exact codex exec exit-code table isn't fully documented, so the robust rule is: treat a non-zero exit as failure, and additionally watch for turn.failed/error events 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:

FieldWhat it counts
input_tokensTokens sent into the model this turn (your prompt + context)
cached_input_tokensThe subset of input tokens served from cache — typically billed cheaper
output_tokensTokens the model generated as visible output
reasoning_output_tokensTokens spent on internal reasoning (separate from visible output)

Why each matters in practice:

  • cached_input_tokens is the lever behind prompt caching. A high cache ratio means you're re-sending the same context (your AGENTS.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_tokens is its own bucket because reasoning effort is configurable (model_reasoning_effort). On a high/xhigh run 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_tokens across every turn.completed gives 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:

bash
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.typeRepresentsUseful fields
agent_messageThe assistant's text outputtext
reasoningA reasoning/thinking step
command_executionA shell command Codex rancommand, status
file_changeA file Codex created/edited/deletedthe change details
mcp_tool_callA call to an MCP server tooltool/result details
web_searchA web search Codex performedquery/results
plan / todo updatesThe agent's plan or todo-list statethe 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_message item and read its text — the structured way to get the same string default stdout would have printed:

    bash
    codex 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_execution item 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, and web_search items 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.

bash
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 can JSON.parse the 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-valid result.json on disk, ready for the next CI step.

A minimal schema looks like this:

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

bash
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
fi

No 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():

typescript
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 schema

The 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-schemarun(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:

  1. Observe the run via the --json event stream — capture thread_id, tally usage for cost, audit command_execution and file_change items, and surface progress to a dashboard.
  2. Trust the result via --output-schema (or SDK outputSchema) — 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 usage totals 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_id and resume to 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:

bash
# 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 present

Tip

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. 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.
  2. 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.
  3. 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.
  4. 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. 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.
  6. 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