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.

stream a run as NDJSON
tofupilot run ./procedures/pcb-fvt --json
example stream (abbreviated)
{"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_started carries protocol_version (currently 1.0). New optional fields can appear without a version bump; ignore unknown fields. Removals and renames bump the version.
  • Ordering: seq starts at 0 and increments by 1 per line. It is the only reliable ordering primitive; timestamps are advisory for display.
  • Terminal invariant: run_finished is always the last event. If the Python subprocess died, run_crashed immediately precedes it. Stop sending stdin commands after run_finished.
  • stderr stays human: progress notes, warnings, and errors go to stderr; stdout carries only event lines.

Event reference

typePurpose
run_startedFirst event. Carries procedure_id and protocol_version.
planOrdered phase list as {key, name} tuples.
phase_startedPhase entered execution: phase_key, attempt, slot_id, started_at.
phase_finishedOutcome, timestamps, duration_ms, recorded measurements, optional error.
phase_skippedPhase never ran (upstream failure or on_first_failure: stop).
phase_logLive log line from a running phase.
ui_requestOperator prompt with the component specs. Reply with ui_response.
ui_auto_continuePrompt auto-resolved (display-only components or pre-baked --ui-values).
ui_timeoutPrompt expired without a response (--ui-timeout).
ui_errorA stdin command was rejected; reason enumerates the cause.
identify_requestUnit identification prompt (serial number, part number).
identify_resolvedIdentification accepted.
identify_timeoutIdentification expired (--ui-timeout).
plug_statusPlug lifecycle transition (initializing, active, error).
plug_logLive log line from a plug's Python service.
measurement_recordedLive measurement write (streaming preview; full records arrive on phase_finished).
attachment_addedPhase attached a file.
state_snapshotReply to get_state: run_status, phase history, any in-flight active_ui_request.
run_upload_queuedRun persisted to the local queue for deferred upload.
run_upload_started / run_upload_succeeded / run_upload_failed / run_upload_droppedUpload lifecycle for queued runs. run_upload_succeeded carries the final run_id and dashboard_url.
internal_warningNon-fatal CLI anomaly, for example payload truncation.
run_crashedSubprocess died before completing; immediately precedes run_finished.
run_finishedLast 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:

CommandPurpose
ui_responseAnswer a ui_request by request_id with component values.
abort_runStop the run. Rejected with ui_error (reason: "invalid_state") before run_started or after run_finished.
get_stateRequest a state_snapshot to recover after reconnecting mid-run.
answer a prompt
{"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: true with serial_number.default_value and part_number.default_value in the procedure .yaml file 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.
unattended CI run
tofupilot run ./procedures/pcb-fvt --json --ui-values ui.json --ui-timeout 300

How is this guide?

On this page