Follow every run

Every run of every kind lands on one record: agent runs, automations, dataset reads, knowledge syncs, Scan. Read its state, its steps, and its events in order, from the same routes Studio uses for its Runs page.

For
Developers and architects
Record
Header · steps · events · children
Order
seq within a run
Read with
GET /runs

01One record for every run

A run has a header, its steps, and its events. The header says what ran, who or what started it, where, and when. Steps are the phases and tool calls, each with a status. Events are the full sequence, numbered from 1.

GET/core/api/v1/organizations/{org_id}/runs/{run_id}

RUNS="$NSTACK_BASE/core/api/v1/organizations/$NSTACK_ORG/runs"
curl -s "$RUNS/$RUN_ID" -H "Authorization: Bearer $NSTACK_SESSION"
Response200 · trimmed · sample values

{
  "run": {
    "run_id": "0b6c1d2e-3f40-4a5b-8c6d-7e8f9a0b1c2d",
    "kind": "agent_run",
    "title": "Test Meeting prep v4",
    "status": "succeeded",
    "actor": { "type": "user", "id": "user_2abc", "label": "Dana Whitfield" },
    "producer": "agents",
    "queued_at": "2026-10-02T14:30:01.902Z",
    "ended_at": "2026-10-02T14:34:19.415Z"
  },
  "steps": [
    { "step_key": "context", "name": "context", "kind": "phase", "status": "succeeded" },
    { "step_key": "tools", "name": "tools", "kind": "phase", "status": "succeeded" },
    { "step_key": "tool:query_accounts#1", "parent_step_key": "tools", "kind": "tool", "status": "succeeded" },
    { "step_key": "tool:search_knowledge#1", "parent_step_key": "tools", "kind": "tool", "status": "succeeded" }
  ],
  "children": []
}
Requires

Read runs, which the Admin role holds today. A member without it reads nothing from these routes.

02The state model

A run's status only moves forward, and a terminal status is final. running and waiting can alternate before the end.

StatusMeansYour code
queuedAccepted, not started. The first event carries the header.Wait.
runningA step is executing.Wait, or read events as they land.
waitingStopped for something outside the run. reason: "approval" means a person must decide.Route it to the person. See escalations.
succeededEvery step finished.Read the output and act.
partialSome steps finished, some did not.Read which steps failed or were skipped.
failedThe run ended on an error.Read the failing step's error. See exceptions.
canceledSomeone stopped it.Nothing to retry unless you decide to.
timed_outIt ran past its limit.Treat like a failure.

03Find runs

List runs newest first and filter by kind, subject_type, subject_id, status, env_id, actor_id, from, and to. The subject is what ran: an agent, an automation, a dataset. Pages come back as {"items": […], "next_cursor": …}.

GET/core/api/v1/organizations/{org_id}/runs

# Everything waiting for a person in PROD
curl -s "$RUNS?status=waiting&env_id=$PROD_ENV_ID" -H "$H"

# One agent's runs this week
curl -s "$RUNS?subject_id=$AGENT_CONFIG_ID&from=2026-10-13T00:00:00Z" -H "$H"

# Everything your system started
curl -s "$RUNS?actor_id=$MACHINE_TOKEN_ID" -H "$H"

const list = async (params: Record) =>
  (await fetch(`${runs}?${new URLSearchParams(params)}`, { headers: auth })).json();

const waiting = await list({ status: "waiting", env_id: prodEnvId });
const failedToday = await list({ status: "failed", from: startOfDay.toISOString() });

04Read events in order

Events are the finest grain: lifecycle, step starts and ends, logs, metrics, outputs, and kind-specific facts such as tool calls. Read them with after_seq and keep the next_after_seq you saw; each call returns 500 by default and 1,000 at most, and you see each event once.

GET/core/api/v1/organizations/{org_id}/runs/{run_id}/events

const TERMINAL = new Set(["succeeded", "partial", "failed", "canceled", "timed_out"]);

export async function followRun(runId: string, onEvent = (e: any) => {}) {
  let after = 0;
  let delay = 1000;
  for (;;) {
    const url = `${runs}/${runId}/events?after_seq=${after}&limit=500`;
    const page = await (await fetch(url, { headers: auth })).json();
    for (const e of page.items) onEvent(e);
    if (page.items.length) { after = page.next_after_seq; delay = 1000; }
    else { delay = Math.min(delay * 2, 15000); }
    const { run } = await (await fetch(`${runs}/${runId}`, { headers: auth })).json();
    if (TERMINAL.has(run.status)) return run;
    if (run.status === "waiting") notifyApprover(run);
    await new Promise((r) => setTimeout(r, delay));
  }
}

import time
TERMINAL = {"succeeded", "partial", "failed", "canceled", "timed_out"}

def follow_run(run_id, on_event=lambda e: None):
    after, delay = 0, 1.0
    while True:
        page = requests.get(f"{runs}/{run_id}/events", params={"after_seq": after, "limit": 500}, headers=auth).json()
        for e in page["items"]:
            on_event(e)
        if page["items"]:
            after, delay = page["next_after_seq"], 1.0
        else:
            delay = min(delay * 2, 15.0)
        run = requests.get(f"{runs}/{run_id}", headers=auth).json()["run"]
        if run["status"] in TERMINAL:
            return run
        if run["status"] == "waiting":
            notify_approver(run)
        time.sleep(delay)
Response200 · sample values

{
  "items": [
    { "run_id": "0b6c…", "seq": 3, "event_id": "5f0c…", "type": "step.started", "step_key": "tool:query_accounts#1", "at": "2026-10-02T14:30:02.118Z", "level": null, "message": null, "data": { "kind": "tool" } },
    { "run_id": "0b6c…", "seq": 4, "event_id": "61d1…", "type": "log", "step_key": "tool:query_accounts#1", "at": "2026-10-02T14:30:02.640Z", "level": "info", "message": "4 rows", "data": {} }
  ],
  "next_after_seq": 4
}

The Events reference lists every type and the envelope each one carries.

05Automation runs and their tasks

The workflow service keeps its own view of an automation's runs, with each task of the graph. Use it when you hold an instance_id, or when you want one automation's history without the rest of the record.

GET/workflow/api/v1/organizations/{org_id}/workflows/{workflow_id}/instances?status=

An automation's runs, filtered by status.

NeedsRead automations

GET/workflow/api/v1/organizations/{org_id}/workflows/instances/{instance_id}

One run and its tasks.

NeedsRead automations

POST/workflow/api/v1/organizations/{org_id}/workflows/instances/{instance_id}/stream-ticket

A single-use ticket, valid 60 seconds at most, to follow the run live over the service's socket. Studio's run strip uses it.

NeedsRead automations

06Counts and trends

For a dashboard, skip the pages and ask for counts. GET …/runs:stats answers run counts and durations by kind, status, or environment, per hour or per day. GET …/scan/runs:stats adds the time Scan routines saved, valued with your organization's cost settings.

GET/core/api/v1/organizations/{org_id}/runs:stats

curl -s "$RUNS:stats" -H "$H"
curl -s "$NSTACK_BASE/core/api/v1/organizations/$NSTACK_ORG/scan/runs:stats" -H "$H"

Nothing here is estimated. The counts come from the same record Studio's Runs page reads.

07Trace it end to end

  • One actor model. {type, id, label} with the type one of user, machine, mcp_key, agent, or system, on every run and every activity entry.
  • One trace. trace_id on a run and on an activity entry joins them to the request that caused them; events carry W3C traceparent.
  • Your correlation. The correlation_id a webhook answers, and the thread_id you give an agent, are yours to key on.
  • Children. A run that starts other runs lists them under children, so an automation's agent runs sit under the automation.

08In design

  • In design An event stream as server-sent events, so your code follows runs and changes without polling.
  • In design A tool_call run per MCP call, with the key as the run's actor.

09Next