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

# Stream executions

> Watch a live execution or job over Server-Sent Events — mint a stream token, connect an EventSource, and reconnect cleanly with the canonical wrapper

Instead of polling `GET /executions/{id}`, watch a run live over [Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/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 /executions/{execution_id}/stream` — one execution, with per-step detail.
* `GET /jobs/{job_id}/stream` — a job 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 /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 /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`.

<CodeGroup>
  ```bash curl theme={null}
  curl -X POST https://api.getdialed.ai/flows/auth/stream-token \
    -H "X-API-Key: $API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"resource_type": "execution", "resource_id": "exec_aabbccdd"}'
  ```

  ```python Python theme={null}
  import os
  import httpx

  res = httpx.post(
      "https://api.getdialed.ai/flows/auth/stream-token",
      headers={"X-API-Key": os.environ["API_KEY"]},
      json={"resource_type": "execution", "resource_id": "exec_aabbccdd"},
  )
  token = res.json()["token"]
  ```
</CodeGroup>

```json theme={null}
{ "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." }
```

| Field           | Type   | Description                        |
| --------------- | ------ | ---------------------------------- |
| `resource_type` | string | `execution` or `job` (closed set). |
| `resource_id`   | string | The execution or job id to stream. |

| Status | Condition                                                       |
| ------ | --------------------------------------------------------------- |
| `401`  | Missing or invalid auth                                         |
| `404`  | Resource not found or not owned by the caller                   |
| `503`  | Stream auth not configured (server `STREAM_TOKEN_SECRET` unset) |

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

<Note>
  **Do not redesign this contract.** Within the 60s TTL, browser-native `EventSource` auto-reconnect (which resends the last frame's `id` as the `Last-Event-ID` header) just works. Past expiry the stream returns `401`; the wrapper re-mints a fresh token and reopens. Because a fresh connection replays the full backlog, the client **must** dedup by `(entity_id, sequence)` — every frame's `id` is `{entity_id}#{sequence}`.
</Note>

```javascript theme={null}
/**
 * Stream one execution (or job) to completion.
 * @param {"execution"|"job"} resourceType
 * @param {string} resourceId  e.g. "exec_aabbccdd" or "job_11223344"
 * @param {() => Promise<string>} getAuthHeader  returns your real "X-API-Key: ..." value
 * @param {(event: {type: string, data: any}) => void} onEvent
 */
async function streamResource(resourceType, resourceId, getApiKey, onEvent) {
  const BASE = "https://api.getdialed.ai/flows";
  const streamPath =
    resourceType === "execution"
      ? `/executions/${resourceId}/stream`
      : `/jobs/${resourceId}/stream`;

  const seen = new Set(); // "(entity_id, sequence)" pairs already rendered
  let lastEventId = null; // "{entity_id}#{sequence}" of the last frame processed
  let es = null;
  let done = false;

  // Mint a fresh 60s stream token for this resource.
  async function mintToken() {
    const res = await fetch(`${BASE}/auth/stream-token`, {
      method: "POST",
      headers: {
        "X-API-Key": await getApiKey(),
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        resource_type: resourceType,
        resource_id: resourceId,
      }),
    });
    if (!res.ok) throw new Error(`stream-token mint failed: ${res.status}`);
    return (await res.json()).token;
  }

  const TERMINAL = new Set(["execution.completed", "execution.failed"]);
  const EVENT_TYPES = [
    "execution.created",
    "step.started",
    "step.completed",
    "step.failed",
    "execution.completed",
    "execution.failed",
  ];

  function handleFrame(type, evt) {
    // Frame id is "{entity_id}#{sequence}" — the Last-Event-ID resume anchor.
    lastEventId = evt.lastEventId;
    const key = evt.lastEventId; // already "(entity_id, sequence)" in one string
    if (seen.has(key)) return; // dedup cross-reconnect repeats (job stream)
    seen.add(key);

    const data = JSON.parse(evt.data);
    onEvent({ type, data });

    // Terminal event: stop. A browser EventSource otherwise reconnects forever.
    if (TERMINAL.has(type)) {
      done = true;
      es.close();
    }
  }

  async function connect() {
    if (done) return;
    const token = await mintToken();
    es = new EventSource(`${BASE}${streamPath}?token=${encodeURIComponent(token)}`);

    for (const type of EVENT_TYPES) {
      es.addEventListener(type, (evt) => handleFrame(type, evt));
    }

    es.onerror = () => {
      if (done) return;
      // readyState CLOSED = fatal (token expired -> 401). CONNECTING = the
      // browser is auto-reconnecting within the TTL (Last-Event-ID header sent
      // for us) — let it. On a fatal close, re-mint and reopen; the full
      // backlog replays and (entity_id, sequence) dedup drops the repeats.
      if (es.readyState === EventSource.CLOSED) {
        es.close();
        connect().catch((err) => onEvent({ type: "error", data: String(err) }));
      }
    };
  }

  await connect();
}
```

## Event-shape reference

Each SSE frame carries `id: {entity_id}#{sequence}`, an `event:` type, and a JSON `data:` payload (`entity_type`, `entity_id`, `job_id`, `event_type`, `sequence`, `step_name`, `step_index`, `payload`, `error_type`, `message`, `truncated`, `created_at`). There are six v1 event types:

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

The job stream (`GET /jobs/{id}/stream`) emits the job'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 /executions/{child_id}/stream`.

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

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

## Next steps

<CardGroup cols={2}>
  <Card title="Execution streaming" icon="tower-broadcast" href="/concepts/execution-streaming">
    The full event taxonomy, reconnect semantics, and truncation contract.
  </Card>

  <Card title="Jobs and executions" icon="diagram-project" href="/concepts/jobs-and-executions">
    The lifecycle and statuses the stream surfaces live.
  </Card>
</CardGroup>
