run --json Event Stream in TofuPilot
Last updated on June 16, 2026
tofupilot run --json emits a newline-delimited JSON (NDJSON) event stream on stdout. Each line is one event object with a type field and a seq counter. The stream is the integration surface for agents, MES bridges, and CI automation: everything the operator UI shows is also on the wire.
tofupilot run ./procedures/pcb-fvt --json{"type":"run_started","procedure_id":"pcb-fvt","protocol_version":"1.0","seq":0}
{"type":"plan","phases":[{"key":"flash","name":"Flash firmware"}],"seq":1}
{"type":"phase_started","phase_key":"flash","attempt":1,"slot_id":"default","started_at":"2026-06-10T08:00:00Z","seq":2}
{"type":"phase_finished","phase_key":"flash","outcome":"PASS","duration_ms":1240,"seq":3}
{"type":"run_finished","outcome":"PASS","exit_code":0,"seq":4}Protocol guarantees
- Versioned:
run_startedcarriesprotocol_version(currently1.0). New optional fields can appear without a version bump; ignore unknown fields. Removals and renames bump the version. - Ordering:
seqstarts at 0 and increments by 1 per line. It is the only reliable ordering primitive; timestamps are advisory for display. - Terminal invariant:
run_finishedis always the last event. If the Python subprocess died,run_crashedimmediately precedes it. Stop sending stdin commands afterrun_finished. - stderr stays human: progress notes, warnings, and errors go to stderr; stdout carries only event lines.
Event reference
type | Purpose |
|---|---|
run_started | First event. Carries procedure_id and protocol_version. |
plan | Ordered phase list as {key, name} tuples. |
phase_started | Phase entered execution: phase_key, attempt, slot_id, started_at. |
phase_finished | Outcome, timestamps, duration_ms, recorded measurements, optional error. |
phase_skipped | Phase never ran (upstream failure or on_first_failure: stop). |
phase_log | Live log line from a running phase. |
ui_request | Operator prompt with the component specs. Reply with ui_response. |
ui_auto_continue | Prompt auto-resolved (display-only components or pre-baked --ui-values). |
ui_timeout | Prompt expired without a response (--ui-timeout). |
ui_error | A stdin command was rejected; reason enumerates the cause. |
identify_request | Unit identification prompt (serial number, part number). |
identify_resolved | Identification accepted. |
identify_timeout | Identification expired (--ui-timeout). |
plug_status | Plug lifecycle transition (initializing, active, error). |
plug_log | Live log line from a plug's Python service. |
measurement_recorded | Live measurement write (streaming preview; full records arrive on phase_finished). |
attachment_added | Phase attached a file. |
state_snapshot | Reply to get_state: run_status, phase history, any in-flight active_ui_request. |
run_upload_queued | Run persisted to the local queue for deferred upload. |
run_upload_started / run_upload_succeeded / run_upload_failed / run_upload_dropped | Upload lifecycle for queued runs. run_upload_succeeded carries the final run_id and dashboard_url. |
internal_warning | Non-fatal CLI anomaly, for example payload truncation. |
run_crashed | Subprocess died before completing; immediately precedes run_finished. |
run_finished | Last event: outcome and exit_code. |
Large payloads are capped: measurement_recorded.value truncates above 1 MB (replaced by {"truncated": true, "original_size_bytes": N} plus an internal_warning).
Stdin commands
The CLI reads NDJSON commands on stdin while the run is active:
| Command | Purpose |
|---|---|
ui_response | Answer a ui_request by request_id with component values. |
abort_run | Stop the run. Rejected with ui_error (reason: "invalid_state") before run_started or after run_finished. |
get_state | Request a state_snapshot to recover after reconnecting mid-run. |
{"type":"ui_response","request_id":"<id from ui_request>","values":{"operator_note":"ok"}}Headless runs
Prompts block the stream until answered. For unattended runs:
- Set
unit.auto_identify: truewithserial_number.default_valueandpart_number.default_valuein the procedure.yamlfile so the identify prompt resolves itself. - Pass
--ui-values <file>to pre-bake answers for UI phases; an empty map{}resolves display-only and acknowledgement prompts. - Pass
--ui-timeout <seconds>so an unanswerable prompt fails the phase instead of hanging forever.
tofupilot run ./procedures/pcb-fvt --json --ui-values ui.json --ui-timeout 300How is this guide?