Skip to main content
Authenticated UI clients watch live workflow progress over Server-Sent Events instead of polling 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.
Two endpoints stream events:
  • 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 browser EventSource 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 an id: {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.
execution.completed and execution.failed are terminal — the server closes the stream after that frame. A browser EventSource otherwise auto-reconnects forever after a close, so the client wrapper must call EventSource.close() on the terminal event. See the stream executions guide for the full contract.

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 (no 409/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 exactly sequence > 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’s id is {entity_id}#{sequence}, so track the rendered pairs and drop repeats.
Past the 60s token expiry the stream returns 401; the client re-mints a fresh token and reopens with its last-seen 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.