# Stream a run

**GET** `/api/runs/{id}/stream`

Sends a run’s progress as it happens, as Server-Sent Events.

Needs [an API token](https://docs.gorunner.io/api/authentication#scopes) with the **Read** permission for runs (`run:read`).

## Parameters

### Path

- `id` · Required · UUID: The run’s ID.

## What arrives

First a `connected` event. Then one unnamed event for each change: a JSON object with `type`, `runId`, `nodeId`, `status`, `data` and `error`. A `: ping` comment comes every 25 seconds; ignore it.

The stream closes after `run_completed`, `run_failed` or `run_cancelled`. A `reconnect` event means connect again, then read the run.

Nothing is replayed: you get only what happens after you connect. Step output isn’t streamed: read it with [Get a run](https://docs.gorunner.io/api/runs/get-run).

## Responses

- 200 OK

  The stream is open. It ends after the run’s last event.

- 204 No Content

  The run is already over, so no stream opens. Read it with Get a run.

- 400 `VALIDATION_ERROR`

  `id` isn’t a UUID. A JSON error, not a stream.

- 403 `FORBIDDEN`

  The token lacks the **Read** permission for runs.

- 404 `NOT_FOUND`

  No run with this ID in the token’s workspace.

- 429 `TOO_MANY_STREAMS`

  This person or token already follows 50 runs on this server. `Retry-After` says when to try again.

- 503 `SERVICE_UNAVAILABLE`

  The run couldn’t be looked up just now. Retry after `Retry-After`; nothing is wrong with the run.

Any call can also answer: [`401 UNAUTHORIZED`](https://docs.gorunner.io/errors#UNAUTHORIZED) · [`401 TOKEN_ENDED`](https://docs.gorunner.io/errors#TOKEN_ENDED) · [`403 TOKEN_SURFACE_OFF`](https://docs.gorunner.io/errors#TOKEN_SURFACE_OFF) · [`403 FEATURE_OFF`](https://docs.gorunner.io/errors#FEATURE_OFF) · [`403 EMBED_NOT_ALLOWED`](https://docs.gorunner.io/errors#EMBED_NOT_ALLOWED) · [`403 EMBED_NOT_AVAILABLE`](https://docs.gorunner.io/errors#EMBED_NOT_AVAILABLE) · [`403 ACCOUNT_SUSPENDED`](https://docs.gorunner.io/errors#ACCOUNT_SUSPENDED) · [`429 RATE_LIMITED`](https://docs.gorunner.io/errors#RATE_LIMITED) · [`503 BUSY`](https://docs.gorunner.io/errors#BUSY) · [`503 PAUSED`](https://docs.gorunner.io/errors#PAUSED). See [Errors](https://docs.gorunner.io/api/errors#by-status).

Request

**curl**

```bash
GORUNNER="https://api.gorunner.io"
GORUNNER_TOKEN="grt_…"  # needs Read for runs
RUN_ID="your-run-id"  # data.id from Run a workflow

curl -N "$GORUNNER/api/runs/$RUN_ID/stream" \
  -H "Authorization: Bearer $GORUNNER_TOKEN" \
  -H "Accept: text/event-stream"
```

**JavaScript**

```js
const res = await fetch(`${GORUNNER}/api/runs/${RUN_ID}/stream`, {
  headers: {
    Authorization: `Bearer ${TOKEN}`,
    Accept: "text/event-stream",
  },
});
if (res.status === 204) {
  // already over: read it with Get a run
}
const reader = res.body.pipeThrough(new TextDecoderStream()).getReader();
for (;;) {
  const { value, done } = await reader.read();
  if (done) break;
  console.log(value); // "data: {…}" lines
}
```

**Python**

```python
import os, requests

GORUNNER = "https://api.gorunner.io"
TOKEN = os.environ["GORUNNER_TOKEN"]
RUN_ID = os.environ["RUN_ID"]

with requests.get(
    f"{GORUNNER}/api/runs/{RUN_ID}/stream",
    headers={"Authorization": f"Bearer {TOKEN}",
             "Accept": "text/event-stream"},
    stream=True,
) as res:
    for line in res.iter_lines(decode_unicode=True):
        if line.startswith("data: "):
            print(line[6:])
```

Response · 200 OK, a step, then the end

```text
event: connected
data: {"runId":"8f0c41d2-6b7e-4c1a-9d3f-2a5b7c9e1f04"}

data: {"type":"step_started","runId":"8f0c41d2-…","nodeId":"parse_csv","status":"running","data":{"attempt":0}}

data: {"type":"step_completed","runId":"8f0c41d2-…","nodeId":"parse_csv","status":"completed","data":{"attempt":0,"duration_ms":3}}

data: {"type":"run_completed","runId":"8f0c41d2-…","status":"completed"}

: ping
```

**Every field**

It takes no body.

### Response · 200 OK, each event

- `type` · string: What happened. `run_completed`, `run_failed` and `run_cancelled` are TERMINAL — nothing follows them, and the server closes the stream after sending one. `run_paused` means the run suspended on a durable wait and may resume later (a `run_resumed` arrives on the SAME run id if it does, though a client that disconnected will have to re-subscribe). `step_failed` is sent for each failed attempt, so a step that will retry sends `step_failed` and then `step_retrying`, which is informational: the step has not failed the run yet. One of `run_started`, `run_completed`, `run_failed`, `run_cancelled`, `run_paused`, `run_resumed`, `step_started`, `step_completed`, `step_failed`, `step_retrying`, `step_skipped`.
- `runId` · UUID: The run this event belongs to.
- `nodeId` · string: For `step_*` events (and `run_paused`/`run_resumed`), the definition step NAME the event is about. Empty on whole-run events.
- `status` · string: The status the subject moved to. Note `retrying` appears here but is NOT a persisted step status. One of `running`, `completed`, `failed`, `cancelled`, `paused`, `skipped`, `retrying`.
- `data` · object or null: Event-specific metadata, never step output. `step_started`: `{attempt}` (none on a Branch or Switch). A wait’s `step_completed` after a resume has none. `step_completed`: `{duration_ms, attempt}` (and `pinned: true` when frozen test data was replayed instead of executing); a branching step’s (Branch or Switch) is `{duration_ms, matchedBranches, matchedBranch}`: the 0-based positions of the arms it took (empty when only Otherwise ran) and the first one’s name, or `otherwise`. `step_retrying`: `{attempt, maxRetries, delay_ms}`. `run_paused`: `{waitStep, resumeMode, resumeAt?}`. No event carries a resume token.
- `error` · string: The failure message on `step_failed`, `step_retrying` and `run_failed`, or the cancellation reason on `run_cancelled`. Absent otherwise.

204 No Content: no body.

**What does a paused run send?**

A `run_paused` event says what the run waits for: `waitStep`, `resumeMode` and, for a set time, `resumeAt`. It never carries a link that decides the run.

Related: [Follow a run as it happens](https://docs.gorunner.io/api/streaming) · [Get a run](https://docs.gorunner.io/api/runs/get-run) · [Run a workflow](https://docs.gorunner.io/api/runs/run-workflow)

Source: https://docs.gorunner.io/api/runs/stream-run
