Codex exec --json event cheatsheet
codex exec --json writes one JSON object per line (JSONL) to stdout. Each line is a top-level thread event with a type field.
codex exec --json writes one JSON object per line (JSONL) to stdout. Each
line is a top-level thread event with a type field.
Below: required + commonly emitted fields for every line type plus a
full-line example for each shape that can be emitted. Fields noted as
optional may be omitted (or null) depending on Codex version and lifecycle.
Unknown fields may appear; ignore what you don’t use.
Top-level event lines (non-item)
thread.started
Fields:
typethread_id
Example:
{"type":"thread.started","thread_id":"0199a213-81c0-7800-8aa1-bbab2a035a53"}
turn.started
Fields:
type
Example:
{"type":"turn.started"}
turn.completed
Fields:
typeusage.input_tokensusage.cached_input_tokens— a subset ofinput_tokensusage.cache_write_input_tokens— added upstream in #33454 (defaults to0)usage.output_tokensusage.reasoning_output_tokens— a subset ofoutput_tokens(never add it on top)
usage is the thread’s running total, not this turn’s: Codex emits
usage_from_last_total(), and exec resume seeds the total from the rollout, so
a resumed run reports every earlier run on the thread + this run. A turn with
no model call reports all zeros; a failed turn reports no usage (its tokens land
in the next run’s total). Verified on 0.157.1 (#419 step-0 capture,
tests/fixtures/codex_0157_resume_usage.jsonl): three runs on one thread
reported input 10656 → 21328 → 32017.
Example (0.157.1):
{"type":"turn.completed","usage":{"input_tokens":24763,"cached_input_tokens":24448,"cache_write_input_tokens":0,"output_tokens":122,"reasoning_output_tokens":64}}
Per-run token attribution in Untether (#419). The runner forwards the raw
thread total; the bridge records it in session_costs.json (key
codex:<thread_id>) and reports usage(run N) − usage(run N−1) for that
thread (per field, with a per-field-max baseline so a lower total never
re-counts). The first run of a fresh thread is source=new_session; a resumed
thread Untether has never seen (e.g. /continue of a thread started in a
terminal) records the raw total once as source=baseline_unknown. Tokens spent
on the thread outside Untether between two Untether runs (a terminal
codex resume <id>, or a rollback window) are inside the next total, so they
land in the next run’s delta as source=ledger — exact in aggregate, not per
run. thread.started is re-emitted with the same thread_id on both
exec resume <id> and exec resume --last (verified on 0.157.1), so reply
resumes and /continue both reach the ledger.
turn.failed
Fields:
typeerror.message
Example:
{"type":"turn.failed","error":{"message":"model response stream ended unexpectedly"}}
error
Fields:
typemessage
Example:
{"type":"error","message":"stream error: broken pipe"}
Note: Codex may emit transient reconnect notices as type="error" with messages
like "Reconnecting... 1/5" while it retries a dropped stream. Treat those as
non-fatal progress updates (the turn continues).
Item event lines (item.*)
Every item line includes:
type(item.started,item.updated, oritem.completed)item.iditem.type- fields for the specific
item.typebelow
item.id is stable for the item; updates/completion reuse the same id.
agent_message (only item.completed)
Fields:
item.textitem.phase(optional) —commentaryfor interim progress text,final_answerfor the answer; absent on older versions
Example:
{"type":"item.completed","item":{"id":"item_3","type":"agent_message","text":"Done. I updated the docs and added examples."}}
reasoning (only item.completed, if enabled)
Fields:
item.text
Example:
{"type":"item.completed","item":{"id":"item_0","type":"reasoning","text":"**Scanning docs for exec JSON schema**"}}
command_execution (item.started and item.completed)
Fields:
item.commanditem.aggregated_outputitem.exit_code(null or omitted until completion)item.status(in_progress,completed,failed,declined)
Example (started):
{"type":"item.started","item":{"id":"item_1","type":"command_execution","command":"bash -lc ls","aggregated_output":"","exit_code":null,"status":"in_progress"}}
Example (completed, success):
{"type":"item.completed","item":{"id":"item_1","type":"command_execution","command":"bash -lc ls","aggregated_output":"docs\nsrc\n","exit_code":0,"status":"completed"}}
Example (completed, failure):
{"type":"item.completed","item":{"id":"item_2","type":"command_execution","command":"bash -lc false","aggregated_output":"","exit_code":1,"status":"failed"}}
Note: aggregated_output is truncated to 64 KiB; truncated output ends with
\n...(truncated).
file_change (only item.completed)
Fields:
item.changes[].pathitem.changes[].kind(add,delete,update)item.status(completed,failed)
Example:
{"type":"item.completed","item":{"id":"item_4","type":"file_change","changes":[{"path":"docs/exec-json-cheatsheet.md","kind":"add"},{"path":"docs/exec.md","kind":"update"}],"status":"completed"}}
mcp_tool_call (item.started and item.completed)
Fields:
item.serveritem.toolitem.arguments(JSON value; defaults tonullif absent)item.result(object ornull; may be omitted)item.result.content(array of MCP content blocks)item.result.structured_content(JSON value ornull)item.error(object ornull; may be omitted)item.error.message(iferroris present)item.status(in_progress,completed,failed)
Example (started):
{"type":"item.started","item":{"id":"item_5","type":"mcp_tool_call","server":"docs","tool":"search","arguments":{"q":"exec --json"},"result":null,"error":null,"status":"in_progress"}}
Example (completed, success):
{"type":"item.completed","item":{"id":"item_5","type":"mcp_tool_call","server":"docs","tool":"search","arguments":{"q":"exec --json"},"result":{"content":[{"type":"text","text":"Found 3 matches.","annotations":{"audience":["assistant"],"lastModified":"2025-01-01T00:00:00Z","priority":0.5}}],"structured_content":{"matches":3}},"error":null,"status":"completed"}}
Example (completed, failure):
{"type":"item.completed","item":{"id":"item_6","type":"mcp_tool_call","server":"docs","tool":"search","arguments":{"q":"exec --json"},"result":null,"error":{"message":"tool timeout"},"status":"failed"}}
web_search (item.started and item.completed)
Fields:
item.id— see the duplicate-id note belowitem.query—""onitem.started; the real query (or"") onitem.completeditem.action— tagged bytype:search(query?,queries?) |open_page(url?) |find_in_page(url?,pattern?) |other.item.startedalways carries{"type":"other"}.item.results(completed only, optional) — array of opaque result objects
Duplicate id key on the wire: upstream serialises ThreadItem { id, #[serde(flatten)] details } plus the item’s own id, so each line carries
"id":"item_N" and then the raw id ("id":"exec-…"). Untether’s decoder
(msgspec) keeps the last key — the raw id shared by started and completed —
so the two phases pair. Pinned by test_web_search_duplicate_id_keeps_last.
Example (0.157.1 wire, trimmed):
{"type":"item.started","item":{"id":"item_2","type":"web_search","id":"exec-8c5b…","query":"","action":{"type":"other"}}}
{"type":"item.completed","item":{"id":"item_2","type":"web_search","id":"exec-8c5b…","query":"codex-cli latest version npm GitHub","action":{"type":"search","query":"codex-cli latest version npm GitHub"},"results":[{"type":"text_result","title":"@openai/codex - npm","url":"https://www.npmjs.com/package/%40openai/codex"}]}}
Legacy (≤ 0.125) lines carry only id + query and still decode.
collab_tool_call (sub-agent coordination)
Fields (all optional except item.id):
item.toolitem.sender_thread_iditem.receiver_thread_ids[]item.promptitem.agents_states(map of thread id →{status, message?})item.status(in_progress,completed,failed)
Untether decodes it but does not render it.
todo_list (item.started, item.updated, and item.completed)
Fields:
item.items[].textitem.items[].completed
Example (started):
{"type":"item.started","item":{"id":"item_8","type":"todo_list","items":[{"text":"Scan docs","completed":false},{"text":"Write cheatsheet","completed":false}]}}
Example (updated):
{"type":"item.updated","item":{"id":"item_8","type":"todo_list","items":[{"text":"Scan docs","completed":true},{"text":"Write cheatsheet","completed":false}]}}
Example (completed):
{"type":"item.completed","item":{"id":"item_8","type":"todo_list","items":[{"text":"Scan docs","completed":true},{"text":"Write cheatsheet","completed":true}]}}
error (non-fatal warning as an item; only item.completed)
Fields:
item.message
Example:
{"type":"item.completed","item":{"id":"item_9","type":"error","message":"command output truncated"}}
Unknown item types
An item.* line with an item.type not listed here decodes as an
unknown_item (id, type and remaining fields kept) rather than failing the
line; Untether does not render it.
MCP content block shapes (mcp_tool_call.result.content)
result.content is an array of content blocks. Each block is one of the
types below; all optional fields may appear depending on the server.
Text content
Fields:
typetextannotations.audience(optional)annotations.lastModified(optional)annotations.priority(optional)
Example block:
{"type":"text","text":"Hello","annotations":{"audience":["assistant"],"lastModified":"2025-01-01T00:00:00Z","priority":0.5}}
Image content
Fields:
typedata(base64)mimeTypeannotations.*(same as above, optional)
Example block:
{"type":"image","data":"<base64>","mimeType":"image/png","annotations":{"audience":["assistant"]}}
Audio content
Fields:
typedata(base64)mimeTypeannotations.*(optional)
Example block:
{"type":"audio","data":"<base64>","mimeType":"audio/wav","annotations":{"audience":["assistant"]}}
Resource link
Fields:
typenameuridescription(optional)mimeType(optional)size(optional)title(optional)annotations.*(optional)
Example block:
{"type":"resource_link","name":"docs/exec.md","uri":"file:///repo/docs/exec.md","description":"Exec docs","mimeType":"text/markdown","size":1234,"title":"exec.md","annotations":{"audience":["assistant"]}}
Embedded resource
Fields:
typeresource(either text or blob contents)annotations.*(optional)
Example block (embedded text):
{"type":"resource","resource":{"uri":"file:///repo/README.md","text":"Hello","mimeType":"text/markdown"},"annotations":{"audience":["assistant"]}}
Example block (embedded blob):
{"type":"resource","resource":{"uri":"file:///repo/image.png","blob":"<base64>","mimeType":"image/png"},"annotations":{"audience":["assistant"]}}
Consumer considerations (rendering + success/failure)
Use this section to decide what to surface to end users vs. what to treat as machine-only metadata.
What to render for users
- Final answer: render
item.completedwhereitem.type = "agent_message"as the main response. - Progress updates (optional):
item.completedwithitem.type = "reasoning"can be shown as brief activity breadcrumbs (only if you want to expose reasoning summaries).item.started/item.completedwithitem.type = "command_execution"can be shown as “running command …” status lines without printing full output.item.completedwithitem.type = "file_change"can be rendered as a list of changed paths and kinds (add/update/delete).item.*withitem.type = "todo_list"can be shown as a progress checklist.
- Errors: render
type = "error"anditem.type = "error"as user-visible warnings or failures.
Fields you can safely skip for UX
command_execution.aggregated_outputis often noisy; many consumers omit or truncate it, and rely oncommand_execution.status+exit_codeinstead.mcp_tool_call.result.contentcan be large and tool-specific; consider showing only high-level status unless you know the tool’s schema.usagefields (turn.completed.usage.*) are the thread’s running total — subtract the previous run’s total for per-run figures (Untether does, #419).
Success and failure signals
- Turn success:
type = "turn.completed"indicates overall success. - Turn failure:
type = "turn.failed"witherror.messageindicates failure. - Item success/failure: use
item.statuson the item payload:command_execution.status:completed= success,failed= failure.file_change.status:completed= patch applied,failed= patch failed.mcp_tool_call.status:completed= tool succeeded,failed= tool failed.
- Fatal stream errors:
type = "error"means the JSONL stream itself hit an unrecoverable error (except transient"Reconnecting... X/Y"notices, which are non-fatal).
Suggested minimal rendering
If you want a compact UI, the following is usually enough:
- Thread/turn lifecycle:
thread.started,turn.started,turn.completedorturn.failed - Final answer:
item.completedwithitem.type = "agent_message" - Optional progress:
item.started/item.completedforcommand_executionandfile_change
Optional/conditional emission notes
turn.failedonly appears on failure; otherwiseturn.completedis emitted.reasoningitems only appear when reasoning summaries are enabled.todo_listitems only appear when the plan tool is active; they are the primary source ofitem.updated.file_changeitems are emitted only asitem.completedin the currentcodex exec --jsonstream;web_searchis emitted onitem.started(empty query,action.type = "other") anditem.completedsince 0.157.x.
Not emitted: context window, per-request usage, compaction
codex exec --json carries no model context window, no per-request token usage
(only the thread total on turn.completed, above) and no compaction event. So
Untether can’t show the N% ctx status segment or the 🗜️ compaction row for
Codex, which the Claude runner does (#819).
Both are tracked in #832
(a rollout-file side-channel, or the app-server protocol).
See also
- Error Reference — actionable hints for common engine errors