GET /executions/{id}. The stream replays the full timeline on connect, then follows the execution live, and closes cleanly on the terminal event — so a detail view can render its entire timeline from the stream alone.
This page is the conceptual reference for the event taxonomy, resume contract, and truncation behavior. The copy-paste
EventSource client wrapper (mint → connect → reconnect → close) lives in the stream executions guide.GET /executions/{id}/stream— a single execution, with per-step detail.GET /jobs/{id}/stream— a job plus each child execution’s lifecycle (no per-step events).
Stream tokens
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.
| Method | Endpoint | Purpose |
|---|---|---|
POST | /auth/stream-token | Mint a 60s token for a resource you own. Any authenticated principal (X-API-Key or Clerk JWT, any role) may mint. |
GET | /executions/{id}/stream | Stream one execution’s lifecycle. Token-only auth (?token=). |
GET | /jobs/{id}/stream | Stream a job plus each child execution’s lifecycle. Token-only auth. |
purpose=stream, org_id, slug, rt (resource type), rid (resource id), iat, and exp = iat + 60. It is stateless — any API pod validates it without a Clerk round-trip — and reusable within its 60s TTL, so browser-native reconnect works. Ownership is verified 404-not-403 before minting: a missing resource and a cross-tenant resource both return 404.
Event taxonomy (v1 — 6 types)
Each SSE frame carries anid: {entity_id}#{sequence} (the resume anchor), an event: <event_type>, and a data: <json> payload with fields like entity_type, entity_id, sequence, step_name, step_index, payload, error_type, message, and truncated.
| 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. |
:keep-alive comment is emitted every 30 seconds to hold the connection open through intermediary idle timeouts. Errors carried to the browser are structured — stack traces, module paths, and SOAP fault dumps stay server-side.
Terminal close and job streams
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 (no409/410 branch).
The job stream emits the job’s own status events plus each child execution’s execution.created / execution.completed / execution.failed — but not per-step events (a 1000-record fan-out emits ~2K child-lifecycle events, not ~10K+ step events). For per-step detail, open the child’s own GET /executions/{child_id}/stream. Only the job’s own terminal event closes the job stream; a child’s terminal does not. An already-terminal job (including a cancelled job) replays whatever history remains, emits a final synthetic terminal frame, and closes.
Reconnect and Last-Event-ID resume
Last-Event-ID has the form {entity_id}#{sequence} — echo the id: of the last frame you processed on reconnect.
- Single-entity execution stream — precise resume. One monotonic per-entity counter, so
Last-Event-ID={execution_id}#{N}resumes at exactlysequence > N: no gap and no re-delivery. - Job stream — full-backlog replay + client dedup. A job and each child have independent sequence counters, but a browser sends only one
Last-Event-ID. A single scalar cursor cannot resume every entity, so the job stream replays the full job backlog on reconnect. This guarantees no missed events — but already-seen events can be re-delivered. The client MUST dedup by(entity_id, sequence): every frame’sidis{entity_id}#{sequence}, so track the rendered pairs and drop repeats.
Last-Event-ID. There is no server-side grace window — the strict 60s posture stays.
Payload truncation and REST fallback
step.completed carries the step output and execution.completed carries the final result. Payloads above a size threshold are truncated with truncated: true on the event; the UI then falls back to GET /executions/{id} for the body.
Honest truncation caveat. When a result exceeds the size cap, both the streamed payload and the persisted
Execution.result are truncated (same size discipline). So the GET /executions/{id} fallback returns the capped representation — not the original oversized body. The full untruncated value is retained only in the platform’s internal execution history; UIs must not assume the REST fallback recovers arbitrarily large outputs.Next steps
Stream executions
The copy-paste EventSource wrapper: mint, connect, reconnect on 401, and close on terminal.
Jobs and executions
The underlying job and execution lifecycle the stream reports on.