Skip to content

Watching a run

A run reports what it is doing as a sequence of events. Each one carries a number, and that number is where you resume from. It is a counter, not a clock: two changes in the same millisecond are two events, and nothing you read depends on our clock agreeing with yours.

GET /katara/v1/orgs/{org_id}/environments/{environment_id}/runs/{id}/events

Send Accept: text/event-stream and you get server-sent events whose id is the sequence. A browser EventSource sends Last-Event-ID when it reconnects, so resumption costs you nothing to write. Without that header the same events come back as one page of JSON, which is what a script wants.

const events = new EventSource(url, { withCredentials: true });
events.addEventListener("run.completed", event => {
const { sequence, snapshot } = JSON.parse(event.data);
console.log(sequence, snapshot?.steps);
});

For a client that keeps its own position, from is the same resumption point:

const page = await client.runEvents(scope, runId, lastSequence);
for (const event of page.data) apply(event);

next_sequence is the number to resume from next time. Passing zero, or leaving from out, says you hold nothing yet; you are given what is still kept rather than being told anything is missing.

A finished run ends the stream instead of holding the connection open. A stream that has been open a long time also ends on its own; reconnect with the last id you saw and you lose nothing.

Once a run has finished, the steps along the way are eventually dropped. What happened to the run is kept. retained_from is the earliest sequence still held, and resuming from below it answers 410 with stream_gone rather than handing you the next thing that still exists, because a gap you could not see is worse than an error you can.

On a 410, read the run itself to see where it got to and carry on from there.

Reading events needs the same permission as reading the run. That permission is rechecked on every page, so access removed while a stream is open ends it.