# Follow a run as it happens

Open an event stream on a run, and see each step start and finish as it happens.

## 1. Start a run

Start a run and keep its ID. Any run works, however it started: [Run a workflow](https://docs.gorunner.io/api/runs/run-workflow), a webhook or a schedule.

Open the stream straight after. It has no replay: an event from before you connected is gone.

```bash
RUN=$(curl -s -X POST "$GORUNNER/api/workflows/$ID/run?environment=production" \
  -H "Authorization: Bearer $GORUNNER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "Ada Lovelace"}' | jq -r .data.id)
```

## 2. Open the stream

Ask for `/api/runs/{id}/stream` with `Accept: text/event-stream`. The token needs `run:read`.

The first event is `connected`. Each one after is a JSON object on a `data:` line. Here the first step had already finished, so the stream starts at the wait.

You should see `run_completed`, then the stream ends by itself.

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

```text
event: connected
data: {"runId":"a274b3d9-…"}

data: {"type":"run_paused","runId":"a274b3d9-…","nodeId":"wait","status":"paused","data":{"resumeAt":"2026-10-25T18:22:09Z","resumeMode":"time","waitStep":"wait"}}

data: {"type":"run_resumed","runId":"a274b3d9-…","nodeId":"wait","status":"running"}

data: {"type":"step_completed","runId":"a274b3d9-…","nodeId":"wait","status":"completed"}

data: {"type":"step_started","runId":"a274b3d9-…","nodeId":"lower","status":"running","data":{"attempt":0}}

data: {"type":"step_completed","runId":"a274b3d9-…","nodeId":"lower","status":"completed","data":{"attempt":0,"duration_ms":0}}

data: {"type":"run_completed","runId":"a274b3d9-…","status":"completed"}
```

## 3. Read the events

- `run_started`: The run started.
- `step_started`: A step started. `data.attempt` counts from 0.
- `step_completed`: A step finished, in `data.duration_ms`. A Branch adds which arms it took: `data.matchedBranches` and `data.matchedBranch`.
- `step_retrying`: A step failed and will try again: `attempt`, `maxRetries`, `delay_ms`, after a `step_failed` for that try.
- `step_failed`, `step_skipped`: A try of a step failed, with `error`, or the step was skipped.
- `run_paused`, `run_resumed`: The run waits on a Delay or a person, then goes on.
- `run_completed`, `run_failed`, `run_cancelled`: The end. Nothing follows.

`nodeId` is the step’s name. A step’s output isn’t in the stream: read it with [Get a run](https://docs.gorunner.io/api/runs/get-run).

**JavaScript**

```js
const res = await fetch(`${GORUNNER}/api/runs/${RUN}/stream`, {
  headers: { Authorization: `Bearer ${TOKEN}`, Accept: "text/event-stream" },
});
if (res.status === 204) process.exit(0); // already over: read the run
const decoder = new TextDecoder();
let buffer = "";
for await (const chunk of res.body) {
  buffer += decoder.decode(chunk, { stream: true });
  let end;
  while ((end = buffer.indexOf("\n\n")) >= 0) {
    const frame = buffer.slice(0, end);
    buffer = buffer.slice(end + 2);
    const data = frame.split("\n").find((l) => l.startsWith("data: "));
    if (!data || /^event:/m.test(frame)) continue; // connected, reconnect
    const ev = JSON.parse(data.slice(6));
    console.log(ev.type, ev.nodeId ?? "", ev.status);
  }
}
```

**Python**

```python
import json, requests

with requests.get(
    f"{GORUNNER}/api/runs/{RUN}/stream",
    headers={"Authorization": f"Bearer {TOKEN}",
             "Accept": "text/event-stream"},
    stream=True,
) as res:
    event = None
    for line in res.iter_lines(decode_unicode=True):
        if line.startswith("event: "):
            event = line[7:]  # connected, reconnect
        elif line.startswith("data: ") and event is None:
            ev = json.loads(line[6:])
            print(ev["type"], ev.get("nodeId", ""), ev["status"])
        elif line == "":
            event = None  # a blank line ends a frame
```

## 4. When it ends, or breaks

- **The run ends**: The server closes the stream after the last event.
- **Already over**: Opening it answers `204 No Content`. Read the run instead.
- `event: reconnect`: The server is going away, or you read too slowly. Connect again, then read the run for what you missed.
- `: ping`: Every 25 seconds, to keep the line open. Ignore it.
- **Too many**: One token may hold 50 streams on one server. The next is `429 TOO_MANY_STREAMS`.

A `run_paused` event never carries a resume link. Read a waiting step’s `resumeUrl` from its output with [Get a run](https://docs.gorunner.io/api/runs/get-run).

## Related

- [Stream a run](https://docs.gorunner.io/api/runs/stream-run): Every response
- [Get a run](https://docs.gorunner.io/api/runs/get-run): Its state and each step’s output
- [Runs](https://docs.gorunner.io/build/runs): Test and production runs, and the six states · 4 min

Source: https://docs.gorunner.io/api/streaming
