GET /executions/{id}, watch a run live over Server-Sent Events. The stream replays the full timeline on connect, follows the execution live, and closes cleanly on the terminal event — so a detail view can render its entire timeline from the stream alone.
Two endpoints stream events:
GET /executions/{execution_id}/stream— one execution, with per-step detail.GET /jobs/{job_id}/stream— a job plus each child execution’s lifecycle (no per-step events).
Why a stream token
A browserEventSource cannot send Authorization / X-API-Key headers on the streaming GET. So the stream is gated by a short-lived (60-second) stream token passed as a ?token= query parameter, minted from POST /auth/stream-token. The token is a self-issued HS256 JWT bound to your org_id and the specific resource; it is stateless, and reusable within its 60s TTL so browser-native reconnect works.
Step 1 — Mint a stream token
POST /auth/stream-token with the resource you want to watch. Any authenticated principal (X-API-Key or Clerk JWT, any role) may mint a token for a resource it owns. Ownership is verified 404-not-403 — a missing resource and a cross-tenant resource both return 404.
| Field | Type | Description |
|---|---|---|
resource_type | string | execution or job (closed set). |
resource_id | string | The execution or job id to stream. |
| Status | Condition |
|---|---|
401 | Missing or invalid auth |
404 | Resource not found or not owned by the caller |
503 | Stream auth not configured (server STREAM_TOKEN_SECRET unset) |
Step 2 — Connect and reconnect (the client contract)
The wrapper below is the canonical browser client. It mints a token, opens anEventSource with the token as a query parameter, dedups by (entity_id, sequence), re-mints on token expiry, and — critically — calls EventSource.close() on the terminal event so the browser does not auto-reconnect forever.
Do not redesign this contract. Within the 60s TTL, browser-native
EventSource auto-reconnect (which resends the last frame’s id as the Last-Event-ID header) just works. Past expiry the stream returns 401; the wrapper re-mints a fresh token and reopens. Because a fresh connection replays the full backlog, the client must dedup by (entity_id, sequence) — every frame’s id is {entity_id}#{sequence}.Event-shape reference
Each SSE frame carriesid: {entity_id}#{sequence}, an event: type, and a JSON data: payload (entity_type, entity_id, job_id, event_type, sequence, step_name, step_index, payload, error_type, message, truncated, created_at). There are six v1 event types:
| Event | Meaning |
|---|---|
execution.created | The execution started. |
step.started | A workflow step began (step_name, step_index). |
step.completed | A step finished; payload carries its output (size-capped — see below). |
step.failed | A step failed; structured error_type + human message (never a Python traceback). |
execution.completed | Terminal — final result in payload; the stream closes after this frame. |
execution.failed | Terminal — structured error; the stream closes after this frame. |
GET /jobs/{id}/stream) emits the job’s own status events plus each child execution’s execution.created / execution.completed / execution.failed — but not per-step events. For per-step detail on a fanned-out record, open that child’s own GET /executions/{child_id}/stream.
Connecting to an already-terminal execution replays its full history ending with the terminal event, then closes — the detail view uses the same stream path regardless of execution state (no
409/410 branch). A :keep-alive comment is emitted every 30 seconds to hold the connection open through the load-balancer idle timer.Next steps
Execution streaming
The full event taxonomy, reconnect semantics, and truncation contract.
Jobs and executions
The lifecycle and statuses the stream surfaces live.