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
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 }
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
}
Action naming
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 — auditors and SIEM rules query on action, so a typo is a missing alert.A single dictionary file makes alerting straightforward:
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
Actor types
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.actor.type | When to use |
|---|---|
'user' | A human authenticated through your normal auth flow. |
'system' | Cron jobs, queue workers, scheduled tasks, internal background processes. |
'api' | Machine-to-machine calls from another service authenticated by a token. |
'agent' | AI tool calls (combine with evlog/ai fields like model, tools, promptId). |
Outcomes
outcome | Meaning |
|---|---|
'success' | The action completed as requested. |
'failure' | The action was attempted but failed (downstream error, race condition, etc.). |
'denied' | The action was rejected by an authorisation check. |
'failure' and 'denied' are different things — auditors care a lot about denied actions because they signal probing or misconfigured access controls. Always log denials (see Recording Events).
Idempotency
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 — it's filled in for you.
Use the field as the primary key in Postgres / Bigtable / DynamoDB so retries stay safe by construction.
Causation and correlation
| Field | Use case |
|---|---|
correlationId | Shared by every audit event that belongs to the same operation (e.g. one HTTP request that triggers a refund + an email + a webhook). |
causationId | The id of the previous audit event that caused this one. Useful for reconstructing chains of cascading actions. |
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
First-class audit logs as a thin layer on top of evlog's wide events. Add tamper-evident audit trails to any app with one enricher, one drain wrapper, and one helper.
Recording
log.audit, log.audit.deny, standalone audit(), withAudit auto-instrumentation, defineAuditAction registries, and auditDiff change patches.