Skip to main content
Instead of polling GET /flows/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 /flows/executions/{execution_id}/stream — one execution, with per-step detail.
  • GET /flows/batches/{batch_id}/stream — a batch plus each child execution’s lifecycle (no per-step events).

Why a stream token

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 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 /account/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.

Step 2 — Connect and reconnect (the client contract)

The wrapper below is the canonical browser client. It mints a token, opens an EventSource 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 just works: the browser resends the last frame’s id as the Last-Event-ID header. How the server uses that header depends on the stream — an execution stream resumes exactly from that sequence (a precise $gt cursor), while a batch stream cannot resume independent per-entity counters from a single header, so it always replays the full retained backlog. Past expiry the stream returns 401; the wrapper re-mints a fresh token and opens a new connection that carries no Last-Event-ID, so the full backlog replays again. Because reconnects can replay already-seen frames, the client must dedup by (entity_id, sequence) — every frame’s id is {entity_id}#{sequence}.

Event-shape reference

Each SSE frame carries id: {entity_id}#{sequence}, an event: type, and a JSON data: payload (entity_type, entity_id, batch_id, event_type, sequence, step_name, step_index, payload, error_type, message, truncated, created_at). There are six v1 event types: The batch stream (GET /flows/batches/{id}/stream) emits the batch’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 /flows/executions/{child_id}/stream.
Truncation and the REST fallback. step.completed and execution.completed 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 full body — but when a result exceeds the size cap, both the streamed payload and the persisted Execution.result are truncated (same size discipline). So the REST fallback returns the capped representation, not the original oversized body. The full, untruncated value is retained only in the platform’s internal execution history. Do not assume the REST fallback recovers arbitrarily large outputs.
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.

Batches and executions

The lifecycle and statuses the stream surfaces live.