Reference · Run Events

Run Events (SSE)

Watch runs progress and finish without polling: two Server-Sent Events streams deliver a snapshot on connect, then lifecycle and step events as they happen. Plain HTTP — no WebSockets, no client library, the browser's native EventSource works as-is.

Endpoints

GET/v1/runs/{runId}/eventsOne run, every event (lifecycle + steps + sub-step phases). Sends a full-run snapshot on connect and closes itself shortly after the terminal event.
GET/v1/runs/events?workspaceId=&projectId=Firehose of your runs — lifecycle events only (created/started/artifact/terminal). Compact snapshot of non-terminal runs on connect; stays open until you close it.

Auth is the same as every other endpoint: Authorization: Bearer with an API key, or the session cookie from a browser. Ownership rules mirror the REST reads — workspace-bound keys only see their workspace.

Subscribe from a backendbash
curl -N https://api.esy.com/v1/runs/run-807ee354/events \
  -H "Authorization: Bearer $ESY_API_KEY"
Subscribe from a browserjs
// Browsers: EventSource can't send headers — auth rides the session cookie.
const stream = new EventSource(
  "https://api.esy.com/v1/runs/" + runId + "/events",
  { withCredentials: true },
);

stream.addEventListener("snapshot", (e) => render(JSON.parse(e.data)));
for (const name of ["run.started", "step.started", "step.phase",
                    "step.completed", "run.completed", "run.failed"]) {
  stream.addEventListener(name, (e) => apply(JSON.parse(e.data)));
}
// IMPORTANT: ignore event names you don't recognize — the contract is additive.

The contract

RuleWhat it means for your client
Snapshot firstThe first event is `snapshot` — the authoritative current state. Replace your local state with it wholesale; every later event is an incremental hint.
Events are state hintsEach event carries enough to render (status, step info, timing). There is no Last-Event-ID resume: on reconnect you simply get a fresh snapshot. Refetch the run if you need detail an event doesn’t carry (e.g. the priced cost ledger).
Additive evolutionNew event names and fields will appear over time. Ignore anything you don’t recognize — never treat an unknown event as an error.
HeartbeatsComment lines (`: hb`) arrive every ~20s. No traffic for more than ~40s means the connection is dead — reconnect.
Terminal closeartifact.created always precedes run.completed / run.review / run.failed, and the terminal event carries artifactId — safe to close on the terminal event.

Event names

EventFires whenOn the firehose?
run.createdrun accepted (pending)yes
run.startedexecution beginsyes
step.started / step.completed / step.failedeach workflow step boundaryno (single-run stream only)
step.phasesub-step checkpoints inside long provider calls (e.g. callingProvider, parsingOutput)no
artifact.createdthe artifact is persistedyes
run.completed / run.review / run.failedterminal stateyes
An event on the wiretext
event: step.completed
data: {
  "type": "step.completed",
  "runId": "run-807ee354",
  "ts": "2026-07-04T19:08:12.412Z",
  "status": "running",
  "templateId": "generate-clip-art-asset-v2",
  "currentStepIndex": 1,
  "stepCount": 3,
  "step": {
    "index": 0, "name": "Render image", "kind": "image",
    "status": "completed", "provider": "openai", "model": "gpt-image-2",
    "durationMs": 14210
  }
}

Recommended client ladder

This is the pattern app.esy.com ships — copy it rather than reinventing it. The worst case at every rung is plain polling, never a broken UI:

  1. Paint fast, then stream. Fire a normal GET /v1/runs/{id} immediately (renders in one round-trip) while the SSE handshake runs in parallel; let the snapshot replace state when it arrives.
  2. On stream error, close the socket, make one authenticated GET through your regular HTTP client — it refreshes an expired session (EventSource can't) and resyncs state — then reconnect with 1s → 8s backoff.
  3. After ~4 consecutive failures, fall back to polling the GET every ~2s and quietly retry the stream every ~30s.
  4. On a terminal event, do one final GET (the priced cost ledger and server-computed timings land there), then close.
Not the same as AI Gateway streaming

/v1/ai/completions also streams — but that stream is the tokens of one completion. Run Events streams the lifecycle of workflow runs. If you are watching a generation finish, you want Run Events.