Activity
A cursor-paginated feed of every autonomously executed action.
Overview
One entry per autonomous execution: a tool call, an approval resolution, an exception filing, a schedule firing. The audit log is principal-centric (who authorized a request: a user or api_key); activity is agent/run-centric. Security-relevant events (a policy deny, a decision-changing guardrail evaluation) stay on the audit log.
No public create endpoint; entries are platform-written. The feed is read-only, append-only, and paginated with an opaque cursor, because offset pages shift under a fast-moving feed.
See the Permissions Reference for the IAM action strings for this module.
Data Model
ActivityEntry
| Field | Type | Description |
|---|---|---|
id | string | Public ID, acte_ prefix |
project_id | string | Owning project |
kind | string | action_executed, approval_created, approval_resolved, exception_created, schedule_fired, share_cap_exceeded, share_resumed, share_revoked, share_suspended, tool_resolution_failed, usage_quantity_invalid |
severity | string | info, warning, critical |
summary | string | Human-readable one-line description |
detail | object | null | Kind-specific structured context (tool id, node id, guardrail policy version) |
orchestration_run_id | string | null | Originating orchestration run, if any |
agent_id | string | null | Associated agent, if any |
generation_id | string | null | Agent generation the entry was produced during, if any |
ref_id | string | null | Producer-specific reference (the approval, exception, share or trigger id the entry came from, or the executed tool's id) |
created_at | string | Append-only timestamp |
orchestration_run_id / agent_id / generation_id / guardrail_version are bare public ids, not foreign keys, matching Exceptions. Node id and guardrail policy version live in detail; the provenance every kind may share (orchestration_run_id, agent_id, generation_id, ref_id) are indexed columns.
Key Concepts
Activity vs. the audit log vs. traces
Three surfaces record what happened:
| Surface | Question it answers | Subject |
|---|---|---|
| Activity (this module) | What did the agents do? | the agent / orchestration run |
| Audit log | Who did what to the platform, and was it allowed? | the principal — a user or API key |
| Traces | How did one generation actually execute? | a single generation's step-by-step tree |
Field-level contrast with the audit log:
| Activity | Audit log | |
|---|---|---|
| Classifier | kind — one of four fixed values; never a permission string | action — is the permission-action string that authorized the request |
| Subject | orchestration_run_id, agent_id, ref_id — no principal is recorded at all | principal_type / principal_id (user / API key) |
| Target | ref_id plus free-form detail | resource_srn + resource_public_id |
| Outcome | severity — records only what happened | status (HTTP), so denied attempts are recorded too, as 403 |
| Request forensics | none | request_id, ip, user_agent |
| Written by | four producers — two event subscriptions, two direct hooks | middleware, once per mutating /api/v1 request that authorizes |
| Immutability | no update path exists, but it is a convention — not enforced by model hooks | hard-enforced append-only, with a retention sweep |
| Reading | keyset cursor pagination, no export | offset pagination plus NDJSON export |
Security-relevant events stay on the audit log even when activity-shaped: a guardrail evaluation that changed a call's outcome is mirrored as a system-originated entry (detail.kind: guardrail_evaluation); see Evaluation Audit Record.
Neither is a superset of the other. A call a guardrail blocked produces an audit record and no action_executed entry; a call that ran produces an action_executed entry while the audit log records the principal who triggered the enclosing request.
Severity
Severity defaults per kind, and a producer may override it:
| Kind | Default severity | Why |
|---|---|---|
action_executed | info | Routine autonomous operation |
approval_created | info | An approval waiting on a human is routine autonomous operation |
approval_resolved | info | Routine autonomous operation |
exception_created | warning | An exception was already filed — an anomaly, by definition |
schedule_fired | info | Routine autonomous operation |
share_cap_exceeded | warning | A call through an accepted share was refused by its cap |
share_resumed | info | An accepted share is granted again |
share_revoked | warning | Something the project accepted stopped being granted, for good |
share_suspended | warning | Something the project accepted stopped being granted |
tool_resolution_failed | warning | The turn ran without tools it was configured to have |
usage_quantity_invalid | warning | A tool call went unpriced because its price row read no valid quantity |
exception_created inherits the filed exception's severity, so a run_failed exception (critical) records a critical entry; the warning default applies only when the event carries no recognized severity. This is the only path that writes critical, so severity=critical surfaces entries a kind filter cannot.
Filtering
kind, severity, agent_id, generation_id and orchestration_run_id are all query parameters on GET /api/v1/activity, and all compose: an entry carries one agent, one generation and one run, so naming two narrows to the entries where both hold.
generation_id is what makes tool_resolution_failed usable during an incident rather than merely alertable — the warning for one suspect turn is a single query, not a scan of the project's feed. action_executed carries the same id, so "everything this turn did" is the same query without a kind.
Cursor pagination
GET /api/v1/activity returns next_cursor; pass it back as cursor. null means no more data. The cursor is an opaque keyset token over (created_at, id), so a page never shifts as entries arrive.
NDJSON export
GET /api/v1/activity/export streams a project's entries as newline-delimited JSON, one entry object per line, for archiving the feed before the retention window expires.
- Oldest first, where the feed itself reads newest first: a feed answers what just happened, and a file is read start to end.
project_idis required; every list filter (kind,severity,agent_id,generation_id,orchestration_run_id) applies identically.- The response streams and pages internally.
- Authorized by
activity:ExportActivity, separate fromactivity:ListActivity.
Retention
Entries are kept indefinitely: no delete endpoint and, unlike the audit log, no pruning sweep, so activity_entries grows with execution volume.
Nothing reads aged entries: the feed pages newest-first, so pruning out of band is safe. Guardrail rate keys read the usage meter, not the feed.
Producers
One producer per kind:
-
action_executed— after a successful tool call, from the orchestration tool-node executor (attributed to run and node,agent_idnull) or the agent tool resolver (attributed to agent and generation, covering conversation, session, and resumed generations). The orchestration path threads no agent identity into the resolver, so a tool node never double-records.Recording sits inside the guardrail interceptor, after the tool returns: a call blocked, tripped, routed to approval, or whose target threw is never recorded. Not recorded: client tools (no server-side execution to attest) and the built-in knowledge-retrieval tools (a knowledge lookup reads, it does not act).
-
approval_created— subscribes toapprovals.created(Approvals), filed while the approval is pending.approvals.expiredis not filed. -
approval_resolved— subscribes toapprovals.approved/approvals.rejected. -
exception_created— subscribes toexceptions.created(Exceptions). -
schedule_fired— from the trigger scheduler's due-firing sweep,source === 'schedule'only; a manual or webhook trigger fire does not produce it. -
share_suspended,share_resumed,share_revoked— from the shares lifecycle, in each consumer project whose acceptance the publisher's suspend, resume, revoke or delete reached.ref_idis the share;detailcarriesshare_id,resourceandpublisher_project_id. -
share_cap_exceeded— from the share cap, in the consumer project whose call through the share was refused.ref_idis the share;detailcarriesshare_id,resource,publisher_project_id,callsandwindow. -
tool_resolution_failed— from the agent tool resolver, when a tool binding contributed no tool to the turn.detailcarriestool_id,tool_type,tool_nameandreason, and the entry'sgeneration_idnames the turn. Thereasonseparates the cases:reasonWhat happened a transport error The mcpserver was unreachable, or a{{secret:…}}/header template would not resolvetools/list answered <status>The listing was refused with a non-OK status tools/list answered the JSON-RPC error <code>: <message>A 200 carrying a JSON-RPC error rather than a result tools/list answered 200 with no `result.tools` arrayA 200 whose body is not a tools/listresulttools/list returned no toolsA well-formed listing of an empty catalogue every tool tools/list returned was excluded by the binding's actions/denied_actionsThe binding's allowlist/denylist left nothing The last two are not transport failures, but the turn still ran without the tools the agent is configured to have, which is the outcome worth monitoring.
The binding is dropped rather than failing the turn — one flaky server must not take an agent down — so the generation completes, with no error and no warning of its own, having answered with fewer tools than it was configured to have. This entry is the operator's signal, and it is the only one that carries the
reason: the turn itself is told only that the named tools are unavailable (see A binding that cannot be resolved), so an upstream status code never becomes text the model can repeat to an end user. SOAT never routes or filters tools per turn, so a turn missing its tools is always this, never a decision the platform made. -
usage_quantity_invalid— from tool metering, in the project a call is metered in, when a resource price row'squantityreads no finite number>= 0; the component is recorded with quantity0and no cost.ref_idis the tool;detailcarriesresource,componentandprice_id.
Every producer is fire-and-forget: a recording failure is logged and never disturbs the action, as in the audit log.
Examples
- CLI
- SDK
- curl
soat list-activity --project-id proj_01 --kind exception_created
# Everything one generation did, warnings included
soat list-activity --project-id proj_01 --generation-id gen_01
# Follow with the returned cursor to page forward
soat list-activity --project-id proj_01 --cursor <next_cursor>
import { SoatClient } from '@soat/sdk';
const soat = new SoatClient({ baseUrl: 'https://api.example.com', token: 'sk_...' });
const { data, error } = await soat.activity.listActivity({
query: { project_id: 'proj_01', generation_id: 'gen_01' },
});
if (error) throw new Error(JSON.stringify(error));
curl -X GET "https://api.example.com/api/v1/activity?project_id=proj_01&generation_id=gen_01" \
-H "Authorization: Bearer <token>"