Events

Every run and every configuration change comes back to you as a CloudEvent. Read a run's events in order, and read changes from a feed whose hash chain you can verify, over the core API.

Format
CloudEvents 1.0, JSON
Two kinds
Run events and audit events
Read with
GET /runs · GET /activity

01How events reach you

Each service records what it does as events, inside your instance. Core collects them into two things your code reads: the run record, one per run with its steps and ordered events, and the activity feed, where each entry carries the hash of the one before it. The routes are the same ones Studio uses for its Runs and Activity pages.

Events · FlowFrom each service to the routes your code reads.

02Topics

TopicCarriesOrdered by
nstack.run.v1The life of every run: queued, started, steps, logs, metrics, outputs, and the terminal event.run_id
nstack.audit.v1Every change to a configuration object: created, updated, published, granted, revoked, and the other verbs.org_id

Events of one run stay in order. An organization's changes stay in order too, which is what the hash chain needs.

03The envelope

Every event carries the CloudEvents 1.0 attributes plus NSTACK extensions. Core validates each one before it lands on the record.

AttributeValue
specversionAlways 1.0.
idA UUID. Core skips an event id it already holds, so a redelivered event applies once.
sourceThe producer: nstack://core/v1, nstack://data/v1, nstack://agents/v1, or nstack://workflow/v1.
typenstack.run.{name}.v1 or nstack.audit.{object_type}.{verb}.v1.
subjectrun/{runid} on run events; {object_type}/{object_id} on audit events.
timeAn RFC 3339 timestamp.
datacontenttypeAlways application/json.
orgidThe organization, org_….
envidThe environment, a UUID, when the event belongs to one.
runid, seqOn run events: the run, and the event's position in it, counting from 1.
actor{actor_type}:{actor_id}, with the type one of user, machine, mcp_key, agent, or system.
traceparentA W3C trace context, so one trace follows a request across services.
dataA JSON object. The whole event is 256 KiB at most.
Run eventnstack.run.step.started.v1 · sample values

{
  "specversion": "1.0",
  "id": "5f0c7f4e-2b1a-4c8e-9d7a-1e2f3a4b5c6d",
  "source": "nstack://agents/v1",
  "type": "nstack.run.step.started.v1",
  "subject": "run/0b6c1d2e-3f40-4a5b-8c6d-7e8f9a0b1c2d",
  "time": "2026-10-02T14:30:02.118Z",
  "datacontenttype": "application/json",
  "orgid": "org_2kXy",
  "envid": "9a8b7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d",
  "runid": "0b6c1d2e-3f40-4a5b-8c6d-7e8f9a0b1c2d",
  "seq": 3,
  "actor": "user:user_2abc",
  "traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
  "data": { "step_key": "context", "name": "context", "kind": "phase" }
}

04Event types

Run events

GroupTypes, between nstack.run. and .v1
Lifecyclequeued, started, waiting, succeeded, partial, failed, canceled, timed_out
Stepsstep.started, step.succeeded, step.failed, step.skipped
Detaillog, metric, output
Kind specificconnect.attempt, config.resolved, rows.batch, chunk.batch, embed.batch, vector.upsert, skill.loaded, retrieval.hits, tool.call, tool.result, generate.segment, approval.requested, approval.decided

A run's status only moves forward, and a terminal status is final. running and waiting can alternate before it ends. The first event, queued or started, carries the run header.

Note

A Scan routine in Safe mode that stops for approval emits waiting with reason: "approval". approval.requested and approval.decided are reserved for the approvals service.

Audit events

The type names the object and the verb: nstack.audit.{object_type}.{verb}.v1. Verbs are created, updated, deleted, published, pinned, unpinned, granted, revoked, rotated, enabled, disabled, and executed. data.changes holds a JSON Patch of the change, with any password, token, or API key written as [redacted].

Objects include workflow, workflow_version, schedule, environment, datasource, datasource_env_config, dataset, mapping, ontology_template, scan_blueprint, product, and setting.

05Facts to build on

  • In order. seq counts from 1 within a run. Read with after_seq and you see each event once.
  • Applied once. Core skips an event id it already holds, so a retry never doubles a step or a log.
  • A chain you can check. Every activity entry stores its hash and the previous one. GET …/activity:verify recomputes every hash over a range of seq and answers ok, checked, head_hash, and first_mismatch_seq.
  • One actor model. The same {type, id, label} actor appears on runs and activity. A run your system started shows its machine token as machine:{token_id}.
  • One trace. trace_id on a run and on an activity entry joins them to the request that caused them.

06Read them

Read a run's events in order with after_seq, and keep the last next_after_seq you saw. Each call returns 500 events by default and 1,000 at most.

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

curl -s "$NSTACK_BASE/core/api/v1/organizations/$NSTACK_ORG/runs/$RUN_ID/events?after_seq=0&limit=500" \
  -H "Authorization: Bearer $NSTACK_SESSION"

const base = `${process.env.NSTACK_BASE}/core/api/v1/organizations/${process.env.NSTACK_ORG}`;
let after = 0;

const res = await fetch(`${base}/runs/${runId}/events?after_seq=${after}&limit=500`, {
  headers: { Authorization: `Bearer ${await getSessionToken()}` },
});
const page = await res.json();
for (const e of page.items) console.log(e.seq, e.type, e.step_key);
after = page.next_after_seq;

import os
import requests

base = f"{os.environ['NSTACK_BASE']}/core/api/v1/organizations/{os.environ['NSTACK_ORG']}"
headers = {"Authorization": f"Bearer {os.environ['NSTACK_SESSION']}"}

page = requests.get(f"{base}/runs/{run_id}/events", params={"after_seq": 0, "limit": 500}, headers=headers).json()
for e in page["items"]:
    print(e["seq"], e["type"], e["step_key"])
after = page["next_after_seq"]
Response200 · sample values

{
  "items": [
    { "run_id": "0b6c1d2e-3f40-4a5b-8c6d-7e8f9a0b1c2d", "seq": 1, "event_id": "1c2d3e4f-5a6b-4c7d-8e9f-0a1b2c3d4e5f", "type": "run.queued", "step_key": null, "at": "2026-10-02T14:30:01.902Z", "level": null, "message": null, "data": {} },
    { "run_id": "0b6c1d2e-3f40-4a5b-8c6d-7e8f9a0b1c2d", "seq": 2, "event_id": "2d3e4f5a-6b7c-4d8e-9f0a-1b2c3d4e5f6a", "type": "run.started", "step_key": null, "at": "2026-10-02T14:30:02.004Z", "level": null, "message": null, "data": {} }
  ],
  "next_after_seq": 2
}

Read changes from the activity feed, newest first, 50 per page by default and 200 at most, with the next_cursor of the last page. To keep your own copy, export a range as NDJSON: each line holds the chain link and the CloudEvent it hashes.

POST/core/api/v1/organizations/{org_id}/activity:export

curl -s "$NSTACK_BASE/core/api/v1/organizations/$NSTACK_ORG/activity" \
  -H "Authorization: Bearer $NSTACK_SESSION"

curl -s -X POST "$NSTACK_BASE/core/api/v1/organizations/$NSTACK_ORG/activity:export" \
  -H "Authorization: Bearer $NSTACK_SESSION" \
  -H "Content-Type: application/json" \
  -d '{"from":"2026-09-01T00:00:00Z","to":"2026-10-01T00:00:00Z"}' > activity.ndjson
One line of activity.ndjsonsample values

{"seq":412,"event_id":"3e4f5a6b-7c8d-4e9f-8a0b-1c2d3e4f5a6b","at":"2026-09-14T09:12:44.310Z","prev_hash":"9f2c…","hash":"a71e…","event":{"specversion":"1.0","type":"nstack.audit.workflow_version.published.v1","subject":"workflow_version/…","…":"…"}}
Requires

A member session with runs read for run events and activity read for the feed. The Admin role holds both. Machine tokens do not reach these routes today.

07In design

  • In design An event stream as server-sent events, so your code follows runs and changes without polling.
  • In design Object events for changes to the records in your ontology, not only to configuration.
  • In design Approval events from the approvals service.
Not yet

NSTACK does not push events to systems outside your instance yet, by webhook or by a broker you run. Poll the two routes above until it does.