
BEST PRACTICES
Audit Logging Best Practices for AI Agents
The four fields every agent audit event needs, how they map onto OCSF, what CloudTrail's digest chain buys you, and the retention defaults of three major platforms against the AI Act's six-month floor.
TL;DR
Most schemas log the gate decision only when the call succeeds, and the denied and timed-out paths quietly fall out of the record. Almost every team logs the agent, the user, and the tool call. Fewer log the gate decision correctly: whether the scope check and the action check passed, captured the same way whether the call succeeded, failed, or timed out.
Most schemas only write a log line for the happy path. The code that decides "no" usually lives in a different layer than the code that writes the record, so a denied attempt or a call that hung for thirty seconds and got dropped never makes it in.
That's backwards. A denied attempt is the moment an agent pushed on a boundary and the boundary held. It's frequently the most useful line in the whole log, and it's the one most commonly missing.
The fix here is the same handful of fields, logged on every path a tool call can take — decided before the first agent reaches production, not reconstructed after the first incident.
Overview
Most agent logging starts as an afterthought. A team ships an agent, something unexpected happens, and only then does anyone ask what was captured. The answer is usually not enough, and not in one place — fields scattered across three services, none of them agreeing on a name for the same concept. That hurts more with agents than with ordinary software, because behavior is emergent: the same prompt can produce different tool calls on different days, and the path taken is rarely obvious from the result. Deciding the schema up front forces the question of who owns emitting each field before an incident demands the answer.
The test for a good design: if a reviewer with no prior context opens a single log entry, can they tell which agent acted, on whose behalf, what it tried to do, and whether it was permitted? If any answer requires opening a second system, the design is incomplete.
What to Log
Four fields form the core of every agent audit event: who acted, for whom, what they attempted, and whether
it was allowed. Those four are aact:audit-granularity/per-action-with-principal-chain,
the finest grade the taxonomy records. Most teams already log the first three. The fourth is where the incomplete schemas come from.
1. Agent Identity
Every action should tie to a specific agent. Shared or generic identities, a single service account used by a dozen automations, collapse the audit trail into one anonymous actor.
If several agents run reporting jobs against a warehouse and all authenticate as analytics-svc, a query that
exfiltrates a sensitive table is untraceable to the agent that ran it. Give each agent a distinct, durable
identity and the same event names the responsible agent immediately.
Log the agent identifier alongside its version: when behavior changes after a deployment, the version field separates "the agent was always doing this" from "this started last Tuesday."
2. User Context
If an agent acts on behalf of a person, the audit trail should capture that person. Agent identity and user identity are separate facts, and both belong in the record: the agent answers what acted, the user context whose authority it borrowed.
This is what makes appropriateness judgable. An HR onboarding agent that provisions accounts is behaving correctly when a recruiter triggers it for a new hire, and suspiciously when it runs for a user with no reason to create accounts. Without the user field, both events look identical.
For autonomous or scheduled agents acting under their own authority, record that explicitly: an empty user field should never be ambiguous about why it's empty.
3. Tool Calls
Capture which tools the agent used, which actions it attempted, and whether those actions succeeded or failed. The failures matter as much as the successes: a procurement agent that tried and failed to issue ten purchase orders is telling you something, even if nothing changed downstream.
A single event might look like this:
{
"timestamp": "2026-08-03T14:22:09Z",
"agent_id": "billing-reconciler",
"agent_version": "2.4.1",
"on_behalf_of": "finance.ops@example.com",
"tool": "erp.invoices",
"action": "post_credit_memo",
"parameters": { "vendor_id": "V-8842", "amount": 4200.00 },
"gate": { "scope_check": "pass", "action_check": "pass" },
"outcome": "success",
"trace_id": "f1c9-7720-aa31"
}Notice the trace_id. Agent activity spans multiple tools, and a shared correlation identifier propagated
through every call in a task is what lets a reviewer pull one identifier and see the whole chain in order: the
request, the tools invoked, the gate decisions, the outcomes. Without it you have a pile of true facts and no
story.
4. The Gate Decision
Every tool call passes through two checks: does the agent's scope include this action at all, and is this specific attempt within that scope's bounds. Log the answer to both, the same way regardless of what happens next.
That last part is where most schemas break down. It's easy to log the checks that passed and the call that
succeeded, and easier still to forget the checks entirely when the call fails downstream or a tool never
responds. Those two paths are where the gate decision matters most: a reviewer needs to know whether the agent
was authorized to attempt the thing that then went wrong. A recorded denial is what the taxonomy means by
aact:failure-behavior/fail-closed; a denial nobody wrote down is indistinguishable afterwards from
aact:failure-behavior/fail-open.
audit store passes -> tool executes success -> log: allowed, success -> audit store failure -> log: allowed, failure -> audit store no response -> log: allowed, timeout -> audit store -->
Figure 1 — Every path through a tool call gets logged, not just the one that succeeds. The gate decision is what makes the denied and timed-out paths legible instead of silent.
The record answers whether the check passed, not whether someone liked the outcome. There's no approval status to capture, because there's no separate approval step on top of the scope: an action is either inside what the agent is permitted to do, or it isn't. When it isn't and a person does the thing instead, that handoff is a different actor under their own identity, and belongs in the trail as its own event. We'd treat the denied and timed-out paths as the more valuable half of this field, not the exception case.
Mapping the Fields to a Standard Schema
Inventing this from scratch is how two services end up disagreeing about what to call the same concept. The
Open Cybersecurity Schema Framework already has a class that fits an agent tool call: API Activity,
class_uid 6003, under the Application Activity category. Its activity_id
normalizes to Create (1), Read (2), Update (3) and Delete (4), and status_id to Success (1), Failure (2) and
Other (99). Those three values plus api and src_endpoint are required on the class, so the tool call and
its outcome are covered without extension.
Identity is where it gets interesting. OCSF's actor object is required, and it describes "the user/role/process
that was the source of the activity". It carries user, session, app_name, app_uid, process, idp and
invoked_by, with a constraint that at least one of app_name, app_uid, invoked_by, process, session
or user must be present. Two details matter for agents. First, the field whose name most obviously fits an
agent, invoked_by, "the name of the service that invoked the activity", has been deprecated since OCSF
v1.2.0; app_name and app_uid are where the acting software belongs now. Second, the object gives you one
user and one application, so the agent-plus-acting-user pair has to be modelled deliberately: the agent in
app_uid, the human in user, and neither borrowing the other's slot.
The gate decision has a home too. OCSF's Authorization Result object has exactly two attributes: decision,
described as "Authorization Result/outcome, e.g. allowed, denied", and an optional policy object for "the
Identity/Access management policies that are applicable". decision is marked recommended rather than
required, which is precisely why the denied path goes missing so often: the standard leaves room for it and
does not insist.
As of OCSF 1.6.0 no event class is specific to a model call or an agent, so API Activity is the closest honest fit, with the agent's own fields carried in the class's extensible attributes. We'd rather extend 6003 than mint a private schema, which stops correlating the moment a second team ships a second agent.
Handling Sensitive Data Carefully
Audit logs shouldn't become a new source of data leakage. An agent handling customer records, payroll data, or source code produces logs as sensitive as the systems they describe, and audit logs are often retained longer and read more widely than the underlying data.
The goal is enough metadata for investigation without copying the payload. Log that an agent read records for
vendor V-8842; don't paste the full bank details into the trail. Reference identifiers, field names, row
counts and hashes carry the investigative value. Redact or tokenize
fields known to contain secrets or personal data, and apply access controls to the logs themselves, not just
to the source systems.
Make Logs Tamper-Resistant
An agent shouldn't be able to modify or delete its own audit trail. If an agent has write access to the system that records its behavior, the record means nothing: the one actor with motive to alter the log is the one being logged.
In practice this means writing audit events to an append-only store the agent can't reach with its operational credentials, separating the log pipeline from the agent's runtime, and protecting deletion with controls no single automated actor can satisfy.
Append-only is a property you can prove. AWS CloudTrail's log file integrity validation hashes every delivered file with SHA-256 and signs an hourly digest file with SHA-256 with RSA, using a different key pair per region. The digest lists the hash of each log file from the last hour, and each digest "also contains the digital signature of the previous digest file if one exists", which chains them: deleting one file or one hour's digest breaks a signature somebody can check. AWS's claim is narrow and honest: the scheme makes it "computationally infeasible to modify, delete or forge CloudTrail log files without detection", and it covers the delete case explicitly.
Figure 2 — Chained digests, as CloudTrail implements them. The previous signature inside each digest is the whole trick: it turns a pile of independently signed files into a sequence with no silent gaps.
RFC 9162, Certificate Transparency version 2.0, generalizes this: append-only Merkle tree logs whose signed tree head commits to the log's size and root hash at a moment in time, and whose consistency proofs let a verifier confirm that a newer head contains every entry the older one did, with nothing removed or reordered. You probably don't need to build that for an internal agent platform. You do need to know that "append-only" without a proof is a configuration setting, not a guarantee.
A log that the logged party can rewrite is not evidence. Tamper resistance is what turns a record into something you can rely on.
Can You Replay the Decision?
Recording a decision and reconstructing one are different requirements, and the second is where most schemas quietly fail. Deterministic replay means that from the record alone, months later, you can say why one specific call was allowed or denied and get the same answer every time. Not why the agent wanted to make the call: the model's reasoning isn't reproducible and doesn't have to be. What has to be reproducible is the gate: these inputs, these rules, this verdict.
That narrowing makes the problem tractable, but it raises the bar on the fields. A replayable decision carries:
- The resolved permission set that was applied, not a pointer to the role or group it came from. Group membership changes. The decision doesn't.
- A version identifier for the rules in force at decision time, so the record names the revision that produced the verdict rather than implying whatever is current.
- Which check decided, and on what. "Scope check failed on
action" is replayable. A baredenyisn't. - The parameters as the gate saw them, after normalization and defaults. What the agent asked for and what the check evaluated are rarely byte-identical.
- A monotonic sequence number issued by the gate, alongside the wall-clock timestamp.
- Agent version and tool-schema version, since the shape of the call is part of what was decided.
- A hash of anything you deliberately didn't keep. Redaction and replay pull against each other, and a hash is the honest compromise.
Clock skew is the first thing that breaks this. NIST's log management guide warns that an inaccurate host clock makes cross-host analysis unreliable, with a worked example: "timestamps might indicate that event A happened 45 seconds before event B, when event A actually happened two minutes after event B." Sync every system to a common time source, then order by the gate's own sequence number within a trace anyway, so timestamps are something humans read rather than something logic depends on.
Rules that changed between the call and the replay produce a confident wrong answer. Feed last quarter's call into this quarter's permissions and you've answered a question nobody asked. Version the rule set, keep each revision immutable and readable for as long as the logs referencing it, and resolve the version from the event rather than from production. Where the rules were never versioned, hash the effective permission set on every decision: the hash won't say what the rules were, but it will say that today's differ from the ones that allowed the call.
Mutable references fail silently. A parameter naming document D-771 is worthless once that document has
been edited, moved, or deleted, while the field still looks healthy. Capture the resolved identifier plus a
content version, ETag, or hash as they stood at call time.
What replay can honestly promise: the authorization decision, reproduced exactly. Not the agent's run. A vendor offering to deterministically replay a whole agent trajectory is describing a recording, and the difference surfaces the first time the model picks a different tool.
Most of this is a schema decision rather than a platform purchase. Two of those fields are nearly impossible to add later: the resolved permission set and the rules version. Nobody reconstructs what a role contained six months ago from a log that never wrote it down.
Retention and Review
Retention is a decision made against your obligations, not a storage default, and the defaults are shorter than most people assume. The EU AI Act sets one explicit floor for high-risk systems: Article 19 requires providers to keep automatically generated logs "for a period appropriate to the intended purpose" and "of at least six months", and Article 26(6) puts the same floor on deployers for the logs under their control.
| Platform | Default window | Configurable? | What to check |
|---|---|---|---|
Google Cloud Logging, _Required bucket | 400 days | No | Admin Activity and System Event logs land here and can't be disabled |
Google Cloud Logging, _Default bucket | 30 days | Yes at project level, 1 to 3,650 days | Data Access logs are off by default outside BigQuery, so there may be nothing to retain |
| AWS CloudTrail, Event history | 90 days | No | Management events only; a trail or event data store is required beyond that |
| AWS CloudTrail Lake | up to 2,557 days (7-year pricing) or 3,653 days (extendable) | Yes, at the event data store | Chosen at creation, and the pricing option decides the ceiling |
| Microsoft Purview, Audit (Standard) | 180 days | Via retention policies | Records generated before 17 Oct 2023 are retained 90 days |
| Microsoft Purview, Audit (Premium) | 1 year for Entra, Exchange, SharePoint and OneDrive; 180 days otherwise | Yes, up to 10 years with the add-on licence | Retention depends on the licence of the user who generated the record |
Table 1 — Audit retention defaults against a six-month regulatory floor. Two of these rows fall below it out of the box, and one of them cannot be raised in place.
A 30-day default and a 90-day default both look fine until the first incident that started four months ago. NIST's log management guide draws the distinction that resolves most of these arguments: log retention is archiving on a schedule as a standard operational activity, while log preservation is deliberately keeping records that would otherwise be discarded because they matter to an investigation. Preservation is an action someone has to take, usually under time pressure.
But retention without review is just cost. A usable trail means structured fields rather than free text, an index that supports the questions investigators actually ask, and alerting on the patterns that matter: repeated denials, unusual user context, spikes in failed tool calls.
A log no one can query is not an audit trail. It's storage.
Where Fabriq Fits
Agentic Fabriq sits on the emitting side of this. It attaches each user's credential server-side at the moment of the call, so every request carries two identities, the agent and the acting user, plus an audit record, and an agent's effective tool list is the intersection of the user's scopes and the agent's. That intersection is what makes the scope half of the gate decision a recorded fact rather than an inference.
It is not a log store, and not a substitute for the append-only design above. The retention window, the digest chain and the index are still yours.
Frequently Asked Questions
What should an AI agent audit log contain? The agent's identity and version, the user whose authority the action was taken under, the tool and action attempted with its key parameters, the gate decision with which check decided, the outcome, a correlation identifier, and a timestamp. The gate decision has to be written on the denied and timed-out paths too.
Is OCSF usable for agent activity? Yes, with a caveat. API Activity (class_uid 6003) fits a tool call,
and the Authorization Result object has a decision attribute for the verdict. No class in OCSF 1.6.0 is
specific to an agent or a model call, so the agent-specific fields ride along on API Activity.
How long should agent audit logs be kept? At least six months if the system is high-risk under the EU AI Act, per Articles 19 and 26(6). Your own incident-response window is usually the binding constraint, and it is almost always longer than your platform's default.
How do I make audit logs tamper-evident rather than just append-only? Hash each record or file, sign a periodic digest, and include the previous digest's signature in the next one, as CloudTrail does hourly. A break in the chain then proves a deletion instead of leaving you to argue about it.
Should prompts and model outputs go in the audit trail? Log the model version, the call the agent made and the verdict; keep payloads out by default. A trail full of prompts is a second copy of your sensitive data with weaker access controls.
Why do denied tool calls never show up in our logs? Because the code that returns "no" usually lives in a different layer from the code that writes the record. Emit the event from the gate itself rather than from the tool client, and the denied, failed and timed-out paths all arrive by construction.
From Activity to Accountability
Good audit logging turns agent activity into an accountable record: who acted, for whom, what they attempted, and whether it was allowed, on every path the attempt could take. Tamper resistance and a shared trace id keep that record trustworthy under scrutiny instead of just large.
Walking into an unfamiliar agent deployment, we wouldn't check the dashboard first. We'd check whether a denied attempt from six months ago is still findable in under a minute. The decisions that separate a real audit trail from a pile of logs aren't difficult, but they're easiest to make before the first agent reaches production and hardest to make after the first incident.
Sources
All URLs read 2026-09-29.
- OCSF API Activity class (6003), schema 1.6.0 — the required attributes, the Create/Read/Update/Delete activity ids and the Success/Failure status ids.
- OCSF Actor object, schema 1.6.0 —
user,session,app_name,app_uid,idp, the at-least-one constraint, andinvoked_bydeprecated since v1.2.0. - OCSF Authorization Result object, schema 1.6.0 —
decisionas a recommended attribute and the optionalpolicyobject. - OCSF categories and classes, schema 1.6.0 — the category and class inventory used to check that no class is specific to agent or model activity.
- NIST SP 800-92, Guide to Computer Security Log Management — the inconsistent-timestamp example, the instruction to sync clocks to a common source, and the retention versus preservation distinction.
- Validating CloudTrail log file integrity, AWS — SHA-256 hashing, SHA-256 with RSA signing, hourly digest files, per-region key pairs, and the previous digest's signature inside each digest.
- CloudTrail concepts, AWS — the 90-day Event history window and the 2,557-day and 3,653-day CloudTrail Lake ceilings.
- Cloud Logging quotas and limits, Google Cloud — the 400-day non-configurable
_Requiredbucket and the 30-day_Defaultbucket. - Cloud Audit Logs overview, Google Cloud — Admin Activity logs that cannot be disabled and Data Access logs disabled by default outside BigQuery.
- Manage audit log retention policies, Microsoft Purview — the 180-day Audit (Standard) default, the 90-day pre-October-2023 records, the one-year Premium default for four workloads, and the 10-year add-on.
- RFC 9162, Certificate Transparency Version 2.0 — append-only Merkle tree logs, signed tree heads and consistency proofs.
- EU AI Act, Article 19 (Automatically generated logs) — the at-least-six-months retention floor for providers of high-risk systems.
- EU AI Act, Article 26 (Obligations of deployers) — paragraph 6, the same six-month floor for logs under a deployer's control.
- Agent Access Control Taxonomy — the audit granularity and failure behavior dimensions;
per-action-with-principal-chainfor the four core fields, andfail-closedagainstfail-openfor why the denial has to be written down. - MCP Security Best Practices, specification 2026-07-28 — why forwarded credentials break attribution: downstream logs "may show requests that appear to come from a different source with a different identity".