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
| Topic | Carries | Ordered by |
|---|---|---|
nstack.run.v1 | The life of every run: queued, started, steps, logs, metrics, outputs, and the terminal event. | run_id |
nstack.audit.v1 | Every 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.
| Attribute | Value |
|---|---|
specversion | Always 1.0. |
id | A UUID. Core skips an event id it already holds, so a redelivered event applies once. |
source | The producer: nstack://core/v1, nstack://data/v1, nstack://agents/v1, or nstack://workflow/v1. |
type | nstack.run.{name}.v1 or nstack.audit.{object_type}.{verb}.v1. |
subject | run/{runid} on run events; {object_type}/{object_id} on audit events. |
time | An RFC 3339 timestamp. |
datacontenttype | Always application/json. |
orgid | The organization, org_…. |
envid | The environment, a UUID, when the event belongs to one. |
runid, seq | On 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. |
traceparent | A W3C trace context, so one trace follows a request across services. |
data | A JSON object. The whole event is 256 KiB at most. |
{
"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
| Group | Types, between nstack.run. and .v1 |
|---|---|
| Lifecycle | queued, started, waiting, succeeded, partial, failed, canceled, timed_out |
| Steps | step.started, step.succeeded, step.failed, step.skipped |
| Detail | log, metric, output |
| Kind specific | connect.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.
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.
seqcounts from 1 within a run. Read withafter_seqand 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:verifyrecomputes every hash over a range ofseqand answersok,checked,head_hash, andfirst_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 asmachine:{token_id}. - One trace.
trace_idon 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.
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"]
{
"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.
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
{"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/…","…":"…"}}
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.
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.