Skip to main content

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​

FieldTypeDescription
idstringPublic ID, acte_ prefix
project_idstringOwning project
kindstringaction_executed, approval_created, approval_resolved, exception_created, schedule_fired, share_cap_exceeded, share_resumed, share_revoked, share_suspended, tool_resolution_failed, usage_quantity_invalid
severitystringinfo, warning, critical
summarystringHuman-readable one-line description
detailobject | nullKind-specific structured context (tool id, node id, guardrail policy version)
orchestration_run_idstring | nullOriginating orchestration run, if any
agent_idstring | nullAssociated agent, if any
generation_idstring | nullAgent generation the entry was produced during, if any
ref_idstring | nullProducer-specific reference (the approval, exception, share or trigger id the entry came from, or the executed tool's id)
created_atstringAppend-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:

SurfaceQuestion it answersSubject
Activity (this module)What did the agents do?the agent / orchestration run
Audit logWho did what to the platform, and was it allowed?the principal — a user or API key
TracesHow did one generation actually execute?a single generation's step-by-step tree

Field-level contrast with the audit log:

ActivityAudit log
Classifierkind — one of four fixed values; never a permission stringaction — is the permission-action string that authorized the request
Subjectorchestration_run_id, agent_id, ref_id — no principal is recorded at allprincipal_type / principal_id (user / API key)
Targetref_id plus free-form detailresource_srn + resource_public_id
Outcomeseverity — records only what happenedstatus (HTTP), so denied attempts are recorded too, as 403
Request forensicsnonerequest_id, ip, user_agent
Written byfour producers — two event subscriptions, two direct hooksmiddleware, once per mutating /api/v1 request that authorizes
Immutabilityno update path exists, but it is a convention — not enforced by model hookshard-enforced append-only, with a retention sweep
Readingkeyset cursor pagination, no exportoffset 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:

KindDefault severityWhy
action_executedinfoRoutine autonomous operation
approval_createdinfoAn approval waiting on a human is routine autonomous operation
approval_resolvedinfoRoutine autonomous operation
exception_createdwarningAn exception was already filed — an anomaly, by definition
schedule_firedinfoRoutine autonomous operation
share_cap_exceededwarningA call through an accepted share was refused by its cap
share_resumedinfoAn accepted share is granted again
share_revokedwarningSomething the project accepted stopped being granted, for good
share_suspendedwarningSomething the project accepted stopped being granted
tool_resolution_failedwarningThe turn ran without tools it was configured to have
usage_quantity_invalidwarningA 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_id is 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 from activity: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_id null) 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 to approvals.created (Approvals), filed while the approval is pending. approvals.expired is not filed.

  • approval_resolved — subscribes to approvals.approved / approvals.rejected.

  • exception_created — subscribes to exceptions.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_id is the share; detail carries share_id, resource and publisher_project_id.

  • share_cap_exceeded — from the share cap, in the consumer project whose call through the share was refused. ref_id is the share; detail carries share_id, resource, publisher_project_id, calls and window.

  • tool_resolution_failed — from the agent tool resolver, when a tool binding contributed no tool to the turn. detail carries tool_id, tool_type, tool_name and reason, and the entry's generation_id names the turn. The reason separates the cases:

    reasonWhat happened
    a transport errorThe mcp server was unreachable, or a {{secret:…}}/header template would not resolve
    tools/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/list result
    tools/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's quantity reads no finite number >= 0; the component is recorded with quantity 0 and no cost. ref_id is the tool; detail carries resource, component and price_id.

Every producer is fire-and-forget: a recording failure is logged and never disturbs the action, as in the audit log.

Examples​

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>