> ## Documentation Index
> Fetch the complete documentation index at: https://docs.getdialed.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Execution streaming

> Watch live workflow progress over Server-Sent Events instead of polling — the conceptual reference for the SSE stream

Authenticated UI clients watch live workflow progress over [Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) instead of polling `GET /executions/{id}`. The stream replays the full timeline on connect, then follows the [execution](/concepts/jobs-and-executions) live, and closes cleanly on the terminal event — so a detail view can render its entire timeline from the stream alone.

<Note>
  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](/guides/stream-executions).
</Note>

Two endpoints stream events:

* `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 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 /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.                                              |

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`.

```bash theme={null}
curl -X POST "$BASE_URL/auth/stream-token" \
  -H "X-API-Key: $GETDIALED_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "resource_type": "execution", "resource_id": "exec_aabbccdd" }'
```

## Event taxonomy (v1 — 6 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`.

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

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.

<Warning>
  `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](/guides/stream-executions) for the full contract.
</Warning>

## 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 (no `409`/`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 exactly `sequence > 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'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 /executions/{id}` for the body.

<Note>
  **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.
</Note>

## Next steps

<CardGroup cols={2}>
  <Card title="Stream executions" icon="signal-stream" href="/guides/stream-executions">
    The copy-paste EventSource wrapper: mint, connect, reconnect on 401, and close on terminal.
  </Card>

  <Card title="Jobs and executions" icon="list-check" href="/concepts/jobs-and-executions">
    The underlying job and execution lifecycle the stream reports on.
  </Card>
</CardGroup>
