Classic painting used as the article cover
← Back to blog

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.

•Aug 3, 2026•Updated Oct 13, 2026•16 min
Audit TrailsLoggingBest Practices

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.

fails

passes

success

failure

no response

Tool call attempted
agent identity + user context

Gate check
scope check + action check

Log: denied
+ which check failed

Tool executes

Log: allowed, success
+ result

Log: allowed, failure
+ error

Log: allowed, timeout
+ elapsed time

Append-only audit store

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.

break in chain

Agent events
(allow, deny, timeout)

Append-only store
agent has no write path

Per-file SHA-256

Hourly digest
hashes + prev signature

Signed with
region key pair

Verifier walks
the digest chain

Deletion or edit
detected

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 bare deny isn'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.

PlatformDefault windowConfigurable?What to check
Google Cloud Logging, _Required bucket400 daysNoAdmin Activity and System Event logs land here and can't be disabled
Google Cloud Logging, _Default bucket30 daysYes at project level, 1 to 3,650 daysData Access logs are off by default outside BigQuery, so there may be nothing to retain
AWS CloudTrail, Event history90 daysNoManagement events only; a trail or event data store is required beyond that
AWS CloudTrail Lakeup to 2,557 days (7-year pricing) or 3,653 days (extendable)Yes, at the event data storeChosen at creation, and the pricing option decides the ceiling
Microsoft Purview, Audit (Standard)180 daysVia retention policiesRecords generated before 17 Oct 2023 are retained 90 days
Microsoft Purview, Audit (Premium)1 year for Entra, Exchange, SharePoint and OneDrive; 180 days otherwiseYes, up to 10 years with the add-on licenceRetention 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.