How do I stream playbook run events?
TL;DR: Runs stream as Server-Sent Events. Every frame shares one envelope where the SSE event: name equals data.type. Parse data.type, handle the run lifecycle (run_start, run_delta, run_stop), the step lifecycle (step_start, step_delta, step_stop), input_request when a run parks, and error frames — and ignore any type you don't recognize.
Who this is for
Developers rendering live progress or driving an agent loop from a playbook run.
Where do events come from?
Two endpoints stream the same shared envelope:
POST /api/playbooks/:id/execute— events for the run you just started, on the same connection.GET /api/playbooks/:id/runs/stream— events for all runs on a playbook (for observers/dashboards).
The /runs/stream connection opens with a stream_start frame (carrying latestSeq, replayed) and emits a ping keepalive roughly every 15s.
What is the run lifecycle?
type | Meaning |
|---|---|
run_start | Run begins. run: { id, playbookId, status: "running", totalSteps, stepId? } |
run_delta | Friendly status / progress update |
run_stop | Result for this segment. run: { id, status, success, totalDurationMs?, output?, error? } — status may be completed, failed, cancelled, or suspended (Paused; not terminal) |
error | Fatal infrastructure fault — always followed by run_stop |
run_delta carries a sub-type: status_delta (user-facing message + phase such as bootstrapping, installing_packages, running_step), progress_delta (stepIndex, stepId, message), or phase_delta (machine phase only).
What is the step lifecycle?
type | Meaning |
|---|---|
step_start | Step opens. step: { id, kind: "code" | "http", language?, description? }, index |
step_delta | Incremental output |
step_stop | Step closes. result.status: completed | failed | skipped |
step_delta sub-types: stdout_delta, stderr_delta, route_delta (taken, kind), and agent_activity_delta.
What is input_request?
When a run parks (operator pause or metaphor.waitForInput), Ballet emits an input_request event with the wake payload — typically requestId, stepId, prompt, schema, optional details, and reason (input_request vs operator_pause). A run_stop with status: "suspended" follows. The sandbox is freed; continue the same runId via Studio or POST /api/runs/:id/reply / /resume.
See How do I wake a suspended run with reply or resume?.
How do I handle errors?
Top-level error frames carry error: { type, message, retryable, stepId? } where type is one of bootstrap_error, executor_error, cancelled, timeout, or internal_error. A fatal error is always followed by a run_stop.
Step failures are not top-level errors — a failed step closes normally via step_stop with result.status: "failed". Check step results in addition to the run result.
How do I keep frames in order?
Each run event after the bus assigns it carries a monotonic seq per playbook channel. Use seq to order or de-duplicate frames if your transport may reorder them.
Example
event: run_start
data: {"type":"run_start","seq":1,"runId":"run-abc","run":{"id":"run-abc","playbookId":"pb-xyz","status":"running","totalSteps":2}}
event: step_start
data: {"type":"step_start","seq":3,"runId":"run-abc","index":0,"step":{"id":"fetch","kind":"code","language":"python"}}
event: step_stop
data: {"type":"step_stop","seq":4,"runId":"run-abc","index":0,"stepId":"fetch","result":{"status":"completed","durationMs":842,"output":{}}}
event: input_request
data: {"type":"input_request","seq":5,"runId":"run-abc","requestId":"req-1","stepId":"approve","prompt":"Approve?","reason":"input_request"}
event: run_stop
data: {"type":"run_stop","seq":6,"runId":"run-abc","run":{"id":"run-abc","status":"suspended","success":false}}
