Skip to main content

Activity

A cursor-paginated feed of every autonomously executed action.

Overview

The activity feed answers "what did agents do today?" — one entry per autonomous execution: a tool call, an approval resolution, an exception filing, a schedule firing. It is distinct from the audit log: the audit log is principal-centric (who authorized a request to the platform — a user or api_key), while activity is agent/run-centric (what an agent did during a run). Security-relevant events (a policy deny, a decision-changing guardrail evaluation) stay on the audit log; only autonomous execution telemetry lands here.

There is no public create endpoint — entries are platform-written by producers. The feed is read-only and append-only, and paginated with an opaque cursor rather than offset/limit, because it is high-volume and 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_resolved, exception_created, schedule_fired
severitystringinfo, warning, critical
summarystringHuman-readable one-line description
detailobject | nullKind-specific structured context (tool id, node id, generation id, guardrail policy version)
run_idstring | nullOriginating orchestration run, if any
agent_idstring | nullAssociated agent, if any
ref_idstring | nullProducer-specific reference (the approval, exception, or trigger id the entry came from, or the executed tool's id)
created_atstringAppend-only timestamp

run_id / agent_id / guardrail_version are held as bare public ids (not foreign keys), matching Exceptions's provenance convention: the feed has no resolution workflow that needs to join back to those rows. A node id, generation id, or guardrail policy version is carried in detail rather than as a dedicated column — only the fields every kind shares (run_id, agent_id, and the generic ref_id) are indexed top-level columns.

Key Concepts

Activity vs. the audit log vs. traces

Three surfaces record "what happened," each answering a different question:

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

Activity and the audit log are the pair most easily confused, because both describe things the platform did on its own. The field-level contrast:

ActivityAudit log
Classifierkind — one of four fixed values; never a permission stringactionis the permission-action string that authorized the request
Subjectrun_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

Because the audit log is the compliance surface and this feed is not, security-relevant events stay there even when they look activity-shaped: a guardrail evaluation that changed a call's outcome is mirrored into the audit log as a system-originated entry (detail.kind: guardrail_evaluation) — see Evaluation Audit Record. Only autonomous-execution telemetry lands here.

Neither surface is a superset of the other, and one tool call can legitimately appear in both. 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 separately records the principal authorized to trigger the enclosing request.

Severity

Severity defaults per kind, and a producer may override it:

KindDefault severityWhy
action_executedinfoRoutine autonomous operation
approval_resolvedinfoRoutine autonomous operation
exception_createdwarningAn exception was already filed — an anomaly, by definition
schedule_firedinfoRoutine autonomous operation

Cursor pagination

GET /api/v1/activity returns next_cursor — pass it back as cursor to fetch the next page; a null next_cursor means there is no more data. The cursor is an opaque, keyset (not offset) token encoding a (created_at, id) position, so a page never shifts as new entries arrive ahead of it — the failure mode an offset page has on a fast-moving, append-only feed.

Producers

Each kind is written by a single, dedicated producer:

  • action_executed — emitted after a successful tool call, from two call sites: the orchestration tool-node executor (attributed to the run and node, agent_id null) and the agent tool resolver (attributed to the agent and generation, so a tool call an agent makes during a generation — in a conversation, a session, or a resumed generation — is recorded too). Each call is recorded by exactly one of them: the orchestration path threads no agent identity into the resolver, so a tool node never double-records.

    Recording sits inside the guardrail interceptor and after the tool returns, which is what makes an entry mean the action really ran: a call that was blocked, tripped, or routed to approval never reaches it, and neither does one whose target threw. Two things are deliberately not recorded: client tools (no server-side execution, so the platform cannot attest the action happened) and the built-in knowledge-retrieval tools (a knowledge lookup reads, it does not act).

  • approval_resolved — subscribes to the existing approvals.approved / approvals.rejected events (see Approvals); no change to that module.

  • exception_created — subscribes to the existing exceptions.created event (see Exceptions); no change to that module.

  • schedule_fired — emitted directly from the trigger scheduler's due-firing sweep, filtered to source === 'schedule' only — a manually- or webhook-fired trigger does not produce this kind.

Every producer is fire-and-forget: a recording failure is logged and swallowed, and never disturbs the action it describes — the same "auditing never blocks the request it describes" principle the audit log follows.

The feed as a guardrail signal

Because action_executed counts real executions, the feed doubles as the platform's autonomous-action rate signal: guardrails read it through soat.activity.actions_1h and soat.activity.actions_24h, the number of action_executed entries in this project over a rolling window ending at evaluation time. That is what lets a guard cap how many actions an agent may take per hour or per day:

{
"class": "B",
"guard": { "<": [{ "var": "soat.activity.actions_24h" }, 200] }
}

Two consequences of the counting rule are worth knowing when writing such a guard:

  • Only action_executed counts. The other three kinds record what the platform did about an action (an approval resolved, an exception filed, a schedule fired), not an action an agent took, so counting them would inflate the rate the ceiling is written against.
  • An empty feed reads as 0, not unresolved. A project that has taken no actions yet passes a rate ceiling rather than failing closed on it — unlike the per-run usage keys, "no actions" is a real, meaningful zero. A query that fails still fails closed.

Examples

soat list-activity --project-id proj_01 --kind exception_created

# Follow with the returned cursor to page forward
soat list-activity --project-id proj_01 --cursor <next_cursor>