Last updated 2026-05-07

SSE Events

The runner exposes a Server-Sent Events stream at GET /api/events. Every state change in every run is multicast to all subscribers — the CLI's stax log -f, the dashboard, and anything else you point at it.

Connection

curl -N http://localhost:4800/api/events

Headers:

Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive

Event types

EventPayloadMeaning
ping{}Sent on connect and every 30 s as keepalive.
run.started{ runId, run }Workflow execution started.
run.completed{ runId, run }Run finished (any conclusion).
job.started{ runId, jobId, job }Job execution started.
job.completed{ runId, jobId, job }Job finished.
step.started{ runId, jobId, stepNumber, step }Step execution started.
step.completed{ runId, jobId, stepNumber, step }Step finished.
step.log{ runId, jobId, stepNumber, line }One stdout/stderr line from a step.

Sample stream

event: ping
data: {}
event: run.started
data: {"runId":"a1b2c3d4","run":{"id":"a1b2c3d4","status":"in_progress","jobs":[]}}
event: job.started
data: {"runId":"a1b2c3d4","jobId":"build","job":{"name":"build","steps":[]}}
event: step.started
data: {"runId":"a1b2c3d4","jobId":"build","stepNumber":1,"step":{"name":"Checkout"}}
event: step.log
data: {"runId":"a1b2c3d4","jobId":"build","stepNumber":1,"line":"Workspace already available\n"}
event: step.completed
data: {"runId":"a1b2c3d4","jobId":"build","stepNumber":1,"step":{"conclusion":"success"}}
event: job.completed
data: {"runId":"a1b2c3d4","jobId":"build","job":{"conclusion":"success"}}
event: run.completed
data: {"runId":"a1b2c3d4","run":{"status":"completed","conclusion":"success"}}

Browser client

const events = new EventSource('http://localhost:4800/api/events');
events.addEventListener('run.started', (e) => {
const { runId, run } = JSON.parse(e.data);
console.log('Run started', runId);
});
events.addEventListener('step.log', (e) => {
const { runId, jobId, stepNumber, line } = JSON.parse(e.data);
appendToTerminal(runId, jobId, stepNumber, line);
});
events.addEventListener('run.completed', (e) => {
const { run } = JSON.parse(e.data);
console.log('Run', run.id, 'finished:', run.conclusion);
});

TypeScript types

type SSEEventType =
| 'run.started'
| 'run.completed'
| 'job.started'
| 'job.completed'
| 'step.started'
| 'step.completed'
| 'step.log';
interface SSEEvent<T = unknown> {
type: SSEEventType;
runId: string;
jobId?: string;
stepNumber?: number;
data: T;
}