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

# Stream task events (SSE)

> Follow task progress in real time with the Server-Sent Events endpoint.

`GET /api/v1/r/tasks/{taskId}/events` opens a **Server-Sent Events**
stream that emits one event per status change or progress tick. The
connection stays open until the task reaches a terminal state
(`completed`, `failed`, `cancelled`), then closes.

Prefer this over polling `GET /r/tasks/{taskId}` — lower latency,
fewer requests.

## Connect

Use a Bearer token (JWT, `sk-`, or `dk-`):

```bash theme={null}
curl -N \
  -H "Authorization: Bearer sk-..." \
  https://core.cyberun.cloud/api/v1/r/tasks/{taskId}/events
```

The browser's built-in `EventSource` does **not** support custom
headers, which means it can't carry an `Authorization` header.
Server-side runtimes (Node `fetch`, Python `requests`, `httpx`),
React Native, or libraries like `eventsource` work fine. In a
browser, proxy through your own backend.

## Event types

| Event       | Meaning                                                                            |
| ----------- | ---------------------------------------------------------------------------------- |
| `status`    | Task status changed (e.g. `pending` → `waiting` → `queued` → `running`).           |
| `progress`  | Execution tick with `progress`, `step_current`, `step_total`, and the active node. |
| `completed` | Task finished. Includes the output object.                                         |
| `failed`    | Task failed. Includes the error string.                                            |
| `cancelled` | Task was cancelled before reaching a terminal state.                               |
| `error`     | Server-side error fetching status. The stream may still continue.                  |

### `status`

```
event: status
data: {"task_id":"...","status":"running","timestamp":"2026-02-01T14:30:05Z"}
```

### `progress`

```
event: progress
data: {
  "task_id":"...",
  "progress":0.35,
  "current_node":"3",
  "step_current":7,
  "step_total":20,
  "timestamp":"2026-02-01T14:30:10Z"
}
```

Nerfstudio tasks add `stage` (`download`, `process_data`, `train`,
`export`, `upload`) and a human-readable `message` to the payload.

| Field          | Type    | Meaning                                                                       |
| -------------- | ------- | ----------------------------------------------------------------------------- |
| `progress`     | number  | `0.0`–`1.0` overall progress.                                                 |
| `step_current` | integer | Steps completed so far (when the runtime reports steps).                      |
| `step_total`   | integer | Total steps for this run.                                                     |
| `current_node` | string  | The runtime's active node identifier (ComfyUI node ID for ComfyUI workflows). |
| `stage`        | string  | Nerfstudio-only. Coarse phase name.                                           |
| `message`      | string  | Nerfstudio-only. Free-form status text.                                       |

### Terminal events

```
event: completed
data: {"task_id":"...","status":"completed","completed_at":"...","output":{...}}

event: failed
data: {"task_id":"...","status":"failed","error":"ComfyUI execution error: ..."}

event: cancelled
data: {"task_id":"...","status":"cancelled"}
```

After any of these three the server closes the connection.

### `error`

```
event: error
data: {"message":"failed to fetch task status"}
```

A transient server-side problem reading task state. Treat it as
recoverable — the task itself may still be running. If the stream
also closes, fall back to polling (see below).

## Example stream

A typical ComfyUI task end-to-end:

```
event: status
data: {"task_id":"...","status":"waiting","timestamp":"..."}

event: status
data: {"task_id":"...","status":"queued","timestamp":"..."}

event: status
data: {"task_id":"...","status":"running","timestamp":"..."}

event: progress
data: {"task_id":"...","progress":0.15,"step_current":3,"step_total":20,"current_node":"3"}

event: progress
data: {"task_id":"...","progress":0.5,"step_current":10,"step_total":20,"current_node":"3"}

event: progress
data: {"task_id":"...","progress":1.0,"step_current":20,"step_total":20,"current_node":"3"}

event: completed
data: {"task_id":"...","status":"completed","output":{...}}
```

## Polling fallback

If your network drops SSE, or you'd rather not hold a connection
open, poll `GET /r/tasks/{taskId}` until `task_status` is one of
`completed`, `failed`, `cancelled`. Three seconds between polls is
a reasonable default.

```bash theme={null}
curl -s \
  -H "Authorization: Bearer sk-..." \
  https://core.cyberun.cloud/api/v1/r/tasks/{taskId}
```

When the task is `completed`, fetch the artifact links via
`GET /r/tasks/{taskId}/result`.

## Consume the stream

<CodeGroup>
  ```python Python theme={null}
  import json, requests

  API_KEY = "sk-..."
  BASE = "https://core.cyberun.cloud/api/v1/r"

  with requests.get(
      f"{BASE}/tasks/{task_id}/events",
      headers={"Authorization": f"Bearer {API_KEY}"},
      stream=True,
  ) as stream:
      event_type = None
      for line in stream.iter_lines(decode_unicode=True):
          if not line:
              continue
          if line.startswith("event:"):
              event_type = line[6:].strip()
          elif line.startswith("data:"):
              data = json.loads(line[5:].strip())
              if event_type == "progress":
                  pct = int(data["progress"] * 100)
                  print(f"{pct}% ({data['step_current']}/{data['step_total']})")
              elif event_type in ("completed", "failed", "cancelled"):
                  print(event_type, data)
                  break
  ```

  ```ts Node.js theme={null}
  const API_KEY = process.env.CYBERUN_KEY!;
  const BASE = 'https://core.cyberun.cloud/api/v1/r';

  const resp = await fetch(`${BASE}/tasks/${taskId}/events`, {
  	headers: { Authorization: `Bearer ${API_KEY}` }
  });

  const reader = resp.body!.getReader();
  const decoder = new TextDecoder();
  let buffer = '';
  let eventType = '';

  while (true) {
  	const { done, value } = await reader.read();
  	if (done) break;
  	buffer += decoder.decode(value, { stream: true });
  	const lines = buffer.split('\n');
  	buffer = lines.pop() ?? '';

  	for (const line of lines) {
  		if (line.startsWith('event:')) {
  			eventType = line.slice(6).trim();
  		} else if (line.startsWith('data:')) {
  			const data = JSON.parse(line.slice(5).trim());
  			if (eventType === 'progress') {
  				console.log(`${Math.round(data.progress * 100)}%`);
  			} else if (eventType === 'completed' || eventType === 'failed' || eventType === 'cancelled') {
  				console.log(eventType, data);
  				return;
  			}
  		}
  	}
  }
  ```
</CodeGroup>

## Related

* [Generate an API key](/cloud/guides/api-key) — issue an `sk-`
  credential.
* [API reference](/api-reference/introduction) — the full endpoint
  catalog.
* [Webhooks](/cloud/guides/webhooks) — push delivery of terminal
  events instead of streaming.
