GET /flows/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 /flows/executions/{id}/stream— a single execution, with per-step detail.GET /flows/batches/{id}/stream— a batch 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 /account/auth/stream-token.
The token is a self-issued HS256 JWT with claims
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 (8 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.
A
: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.
The park frames
Both frames carry the same key set as every other frame, but the only fields they populate are the ones identifying the execution —entity_type, entity_id, event_type, sequence and created_at. Every content-bearing field is null: no payload, no step_name, no step_index, no error_type and no message.
That is a contract, not an accident of the current implementation. A park frame must never become a side channel for whatever the run is waiting on, and the message a send-and-wait expects an answer to is exactly the kind of content that would otherwise leak through one. Read GET /flows/executions/{id} for the run’s state.
Neither frame closes the stream, and a client must not treat them as an ending. A park can last days; the connection stays open through it on the same 30-second keep-alive as any other idle period, and
execution.resumed picks the timeline back up. Only execution.completed and execution.failed are terminal.A run may park and resume more than once — a flow with several waiting steps emits a pair per park.Terminal close and batch 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 batch stream emits the batch’s own status events plus each child execution’s execution.created / execution.completed / execution.failed — but not per-step events, and not the child park frames (execution.waiting / execution.resumed), which follow the same rule for the same reason. A batch-level view cannot tell you that one particular child is parked; open that child’s own stream, or read its execution, for that (a 1000-record fan-out emits ~2K child-lifecycle events, not ~10K+ step events). For per-step detail, open the child’s own GET /flows/executions/{child_id}/stream. Only the batch’s own terminal event closes the batch stream; a child’s terminal does not. An already-terminal batch (including a cancelled batch) 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. - Batch stream — full-backlog replay + client dedup. A batch 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 batch stream replays the full batch 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 /flows/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 /flows/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.
Batches and executions
The underlying batch and execution lifecycle the stream reports on.