Activity
What the agents in a project actually did, newest first.
Overview
One entry per autonomous act: a tool call that executed, an approval someone
resolved, an exception that was filed, a schedule that fired. Each carries a
one-line summary you can read at a glance and a detail object with the
structured context behind it.
The feed is read-only and append-only — entries are written by the runtime, never by a caller — and it is paged with an opaque cursor rather than an offset, because a fast-moving feed shifts under offset pages.
This module is a verbatim mirror of the runtime: every field, method, status code and error shape is the runtime's own, re-rooted under the project in the path.
See the OpenAPI spec for the full endpoint and schema reference, or browse it rendered under API Reference → Activity.
Data Model
ActivityEntry
| Field | Type | Description |
|---|---|---|
id | string | Public entry ID (acte_ prefix). |
project_id | string | The owning project. |
kind | string | action_executed, approval_created, approval_resolved, exception_created, schedule_fired or tool_resolution_failed. |
severity | string | info, warning or critical. |
summary | string | One-line, human-readable description. |
detail | object, nullable | Kind-specific context — the tool, the node, the generation, the guardrail version. |
orchestration_run_id | string, nullable | The orchestration run this happened in, if any. |
agent_id | string, nullable | The agent involved, if any. |
generation_id | string, nullable | The generation the entry was produced during, if any. |
ref_id | string, nullable | The producing record — the approval, exception or trigger firing the entry came from, or the executed tool. |
created_at | string (date-time) |
Key Concepts
Three surfaces record "what happened"
They are easy to confuse, and each answers a different question:
| Surface | Question | Subject |
|---|---|---|
| Activity (this module) | What did the agents do? | the agent or orchestration run |
| Audit log | Who asked the platform for what, and was it allowed? | the credential behind a request |
| Traces | How did one generation execute, step by step? | a single generation |
Neither of the first two is a subset of the other, and one tool call can appear in
both. A call a guardrail blocked leaves an audit record and
no action_executed entry — nothing executed. A call that ran leaves an
action_executed entry here, while the audit log separately records the request
that set it off.
The sharpest difference is the classifier: kind here is one of six fixed
values and never a permission string, while the audit log's action is the
permission string that authorized the request. This feed records what happened;
the audit log records decisions, including refusals.
Severity is not a restatement of kind
Severity defaults per kind — info for the routine ones, warning for
exception_created and tool_resolution_failed — but an exception_created
entry inherits the filed exception's own severity, so it spans
all three values. That is the only path that writes critical, which makes
severity=critical a filter worth having: it surfaces the entries a kind
filter cannot express.
A dropped tool binding is recorded here
When a tool binding contributes no tool to a turn, the runtime leaves it out and
runs the turn anyway — one unreachable server must not take an agent down. The
generation then completes carrying no error, over a
trace with no children, having answered without a capability it
was configured to have. A tool_resolution_failed entry, severity warning, is
the only record that anything was missing.
It covers both halves of "contributed nothing": a binding that produced no
listing at all — the server unreachable or refusing the credential, or an
unresolvable {{secret:…}} in its URL or headers — and a listing that was read
and left nothing to attach. Only an mcp tool can fail this way;
the other types need no network to resolve, and one whose endpoint is down fails
when the model calls it instead.
detail names the binding and the turn:
| Key | Description |
|---|---|
tool_id | The tool, null for a binding defined inline on the agent. Repeated on ref_id. |
tool_type | The binding's type. |
tool_name | Its name, as summary reads it. |
reason | Why nothing attached — tools/list answered 401, tools/list returned no tools, an unresolved secret reference. |
agent_id is the agent whose turn it was, generation_id the turn itself, and
orchestration_run_id the run it happened inside, if
any — all three are fields on the entry rather than detail keys, which is what
makes them filterable.
Filters compose
kind, severity, agent_id, generation_id and orchestration_run_id are
all composable, and an entry carries at most one of each subject, so naming two
narrows to the rows where both hold.
generation_id is what turns this feed from alertable into usable during an
incident: given a generation that answered oddly, one query
returns everything that turn did — including whether a binding was dropped.
Cursor pagination
GET /v1/projects/{project_id}/activity
returns next_cursor; pass it back as cursor for the next page, and a null
next_cursor means you have reached the end. The cursor encodes a position, not
an offset, so a page does not shift as new entries land ahead of it.
The feed is also a guardrail input
A guardrail can read a rolling rate of
tool calls — runtime.projects.tool_calls.1h, runtime.agents.tool_calls.24h,
runtime.tools.tool_calls.7d — so "an agent that has done an unusual amount in
the last hour" is expressible as a gate rather than only as something to notice
afterwards. This feed is where you read what those calls did.
Retention
Entries are kept indefinitely. There is no delete endpoint and no sweep, so the feed grows with autonomous execution volume.
Examples
Read what happened, newest first
- CLI
- SDK
- curl
naturali list-activity \
--project-id proj_V1StGXR8Z5jdHi6B \
--limit 20
const { data: feed } = await naturali.activity.listActivity({
path: { project_id: 'proj_V1StGXR8Z5jdHi6B' },
query: { limit: 20 },
});
for (const entry of feed?.data ?? []) {
console.log(entry.created_at, entry.kind, entry.summary);
}
curl "https://api.naturali.ai/v1/projects/proj_V1StGXR8Z5jdHi6B/activity?limit=20" \
-H "Authorization: Bearer $NATURALI_TOKEN"
Surface only what went wrong
- CLI
- SDK
- curl
naturali list-activity \
--project-id proj_V1StGXR8Z5jdHi6B \
--severity critical
const { data: feed } = await naturali.activity.listActivity({
path: { project_id: 'proj_V1StGXR8Z5jdHi6B' },
query: { severity: 'critical' },
});
for (const entry of feed?.data ?? []) {
console.log(entry.summary, entry.ref_id);
}
curl "https://api.naturali.ai/v1/projects/proj_V1StGXR8Z5jdHi6B/activity?severity=critical" \
-H "Authorization: Bearer $NATURALI_TOKEN"
Alert on a tool binding that did not resolve
Nothing else reports this: poll or alert on the kind itself.
- CLI
- SDK
- curl
naturali list-activity \
--project-id proj_V1StGXR8Z5jdHi6B \
--kind tool_resolution_failed
const { data: feed } = await naturali.activity.listActivity({
path: { project_id: 'proj_V1StGXR8Z5jdHi6B' },
query: { kind: 'tool_resolution_failed' },
});
for (const entry of feed?.data ?? []) {
console.log(entry.created_at, entry.summary, entry.detail);
}
curl "https://api.naturali.ai/v1/projects/proj_V1StGXR8Z5jdHi6B/activity?kind=tool_resolution_failed" \
-H "Authorization: Bearer $NATURALI_TOKEN"
An entry means a generation ran with fewer tools than its
agent declares; detail.generation_id is the one to look at and detail.reason
is what to fix. An empty page over a window means every binding attached.
Read everything one generation did
Given a generation that answered oddly, this is the query that says whether it ran with every tool it was meant to have.
- CLI
- SDK
- curl
naturali list-activity \
--project-id proj_V1StGXR8Z5jdHi6B \
--generation-id gen_V1StGXR8Z5jdHi6B
const { data: feed } = await naturali.activity.listActivity({
path: { project_id: 'proj_V1StGXR8Z5jdHi6B' },
query: { generation_id: 'gen_V1StGXR8Z5jdHi6B' },
});
for (const entry of feed?.data ?? []) {
console.log(entry.kind, entry.summary);
}
curl "https://api.naturali.ai/v1/projects/proj_V1StGXR8Z5jdHi6B/activity?generation_id=gen_V1StGXR8Z5jdHi6B" \
-H "Authorization: Bearer $NATURALI_TOKEN"
Page through with the cursor
- CLI
- SDK
- curl
naturali list-activity \
--project-id proj_V1StGXR8Z5jdHi6B \
--cursor eyJjcmVhdGVkX2F0IjoiMjAyNi0wOC0yMSJ9
let cursor: string | undefined;
do {
const { data: page } = await naturali.activity.listActivity({
path: { project_id: 'proj_V1StGXR8Z5jdHi6B' },
query: { cursor, limit: 100 },
});
for (const entry of page?.data ?? []) console.log(entry.summary);
cursor = page?.next_cursor ?? undefined;
} while (cursor);
curl "https://api.naturali.ai/v1/projects/proj_V1StGXR8Z5jdHi6B/activity?cursor=eyJjcmVhdGVkX2F0IjoiMjAyNi0wOC0yMSJ9" \
-H "Authorization: Bearer $NATURALI_TOKEN"