---
title: "How do I stream playbook run events?"
description: "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 (runstart, rundelta, runstop), the step lifecycle (stepstart, stepdelta, stepstop), input_request when a run parks, and error frames — and ignore any type you don't recognize."
canonical_url: "https://docs.ballet.dev/articles/how-do-i-stream-playbook-run-events-SsXLWiWt9X"
md_url: "https://docs.ballet.dev/articles/how-do-i-stream-playbook-run-events-SsXLWiWt9X.md"
---
# 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?](/articles/how-do-i-wake-a-suspended-run-with-reply-or-resume-EDYuheV1x4).

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

```sse
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}}
```

## Related articles

- [How do I wake a suspended run with reply or resume?](/articles/how-do-i-wake-a-suspended-run-with-reply-or-resume-EDYuheV1x4)
- [How do I pause and resume a playbook run?](/articles/how-do-i-pause-and-resume-a-playbook-run-TK6ueiBF0d)
- [Run playbooks over the REST API](/articles/how-do-i-run-a-playbook-over-the-rest-api-KQIz0apagm)
- [How do I authenticate with the Ballet API?](/articles/how-do-i-authenticate-with-the-ballet-api-tWaqCPLyGT)
- [How do I read runs and run history?](/articles/how-do-i-read-runs-and-run-history-WukfYP5du3)
