---
title: "Audit Schema"
description: "The AuditFields type, action naming conventions, actor types, idempotency, and how the schema sits inside a regular wide event."
canonical_url: "https://www.evlog.dev/use-cases/audit/schema"
---
# Audit Schema

> The AuditFields type, action naming conventions, actor types, idempotency, and how the schema sits inside a regular wide event.

`event.audit` is a typed field on every wide event. Downstream queries filter on `audit IS NOT NULL` to materialise an audit dataset out of regular logs.

## `AuditFields` type

<code-collapse>

```typescript
interface AuditFields {
  action: string                          // 'invoice.refund'
  actor: {
    type: 'user' | 'system' | 'api' | 'agent'
    id: string
    displayName?: string
    email?: string
    // For type === 'agent', mirrors evlog/ai fields:
    model?: string
    tools?: string[]
    reason?: string
    promptId?: string
  }
  target?: { type: string, id: string, [k: string]: unknown }
  outcome: 'success' | 'failure' | 'denied'
  reason?: string
  changes?: { before?: unknown, after?: unknown, patch?: AuditPatchOp[] }
  causationId?: string                    // ID of the action that caused this one
  correlationId?: string                  // Shared by every action in one operation
  version?: number                        // Defaults to 1
  idempotencyKey?: string                 // Auto-derived; safe retries across drains
  context?: {                             // Filled by auditEnricher
    requestId?: string
    traceId?: string
    ip?: string
    userAgent?: string
    tenantId?: string
  }
  signature?: string                      // Set by signed({ strategy: 'hmac' })
  prevHash?: string                       // Set by signed({ strategy: 'hash-chain' })
  hash?: string
}
```

</code-collapse>

## Action naming

<tip>

**Naming convention for action.** Use `noun.verb` (`invoice.refund`, `user.invite`, `apiKey.revoke`). Past tense if the action already happened (`invoice.refunded`), present tense if `withAudit()` will resolve the outcome. Keep a small fixed dictionary in one file, because auditors and SIEM rules query on `action`, so a typo is a missing alert.

</tip>

A single dictionary file makes alerting straightforward:

```typescript [src/audit/actions.ts]
export const AUDIT_ACTIONS = {
  USER_INVITE: 'user.invite',
  USER_REMOVE: 'user.remove',
  USER_ROLE_CHANGE: 'user.role-change',
  INVOICE_REFUND: 'invoice.refund',
  API_KEY_REVOKE: 'apiKey.revoke',
} as const
```

<tip>

For more than a handful of actions, prefer [`defineAuditCatalog`](https://www.evlog.dev/use-cases/audit/recording#defineauditcatalog) over a plain object dictionary: same `noun.verb` convention, but you get autocomplete on the action union and per-entry `target` type inference for free.

</tip>

## Say who acted

<warning>

**Don't fake the actor.** Use `actor.type: 'system'` for cron jobs, queue workers, and background tasks; `actor.type: 'api'` for machine-to-machine calls authenticated by a token; `actor.type: 'agent'` for AI tool calls. Logging a synthetic `'user'` for system actions is the single fastest way to fail an audit review.

</warning>

<table>
<thead>
  <tr>
    <th>
      <code>
        actor.type
      </code>
    </th>
    
    <th>
      When to use
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        'user'
      </code>
    </td>
    
    <td>
      A human authenticated through your normal auth flow.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        'system'
      </code>
    </td>
    
    <td>
      Cron jobs, queue workers, scheduled tasks, internal background processes.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        'api'
      </code>
    </td>
    
    <td>
      Machine-to-machine calls from another service authenticated by a token.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        'agent'
      </code>
    </td>
    
    <td>
      AI tool calls (combine with <code>
        evlog/ai
      </code>
      
       fields like <code>
        model
      </code>
      
      , <code>
        tools
      </code>
      
      , <code>
        promptId
      </code>
      
      ).
    </td>
  </tr>
</tbody>
</table>

## Record how it ended

<table>
<thead>
  <tr>
    <th>
      <code>
        outcome
      </code>
    </th>
    
    <th>
      Meaning
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        'success'
      </code>
    </td>
    
    <td>
      The action completed as requested.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        'failure'
      </code>
    </td>
    
    <td>
      The action was attempted but failed (downstream error, race condition, etc.).
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        'denied'
      </code>
    </td>
    
    <td>
      The action was rejected by an authorisation check.
    </td>
  </tr>
</tbody>
</table>

`'failure'` and `'denied'` are different things, and auditors care a lot about denied actions because they signal probing or misconfigured access controls. Always log denials (see [Recording Events](https://www.evlog.dev/use-cases/audit/recording#logauditdeny)).

## Make a retry safe

`idempotencyKey` is auto-derived from a hash of `action`, `actor.id`, `target`, and a coarse timestamp. The result: even if your drain retries an audit insert across a network blip, the duplicate row collapses on `ON CONFLICT DO NOTHING`. You don't have to think about it, since it is filled in for you.

Use the field as the primary key in Postgres / Bigtable / DynamoDB so retries stay safe by construction.

## Tie an action to what caused it

<table>
<thead>
  <tr>
    <th>
      Field
    </th>
    
    <th>
      Use case
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        correlationId
      </code>
    </td>
    
    <td>
      Shared by every audit event that belongs to the <strong>
        same operation
      </strong>
      
       (e.g. one HTTP request that triggers a refund + an email + a webhook).
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        causationId
      </code>
    </td>
    
    <td>
      The id of the previous audit event that <strong>
        caused
      </strong>
      
       this one. Useful for reconstructing chains of cascading actions.
    </td>
  </tr>
</tbody>
</table>

Most teams set `correlationId` to `requestId`. `causationId` is opt-in and only worth filling when a single user action triggers many internal audit events.

---

- [Overview](https://www.evlog.dev/use-cases/audit/overview)
- [Recording](https://www.evlog.dev/use-cases/audit/recording)


## Sitemap

See the full [sitemap](https://www.evlog.dev/sitemap.md) for all pages.
