Audit Log
Every gateway call_tool request — regardless of decision — produces an immutable audit record in PostgreSQL. The record is written before the response is returned to the agent. There are no gaps in the trail.
Audit event schema
The audit_events table. Fields a compliance engineer will care about:
| Column | Type | Description |
|---|---|---|
id | UUID | Immutable event identifier |
session_id | UUID | Groups all tool calls within one agent run |
sequence_number | integer | Call order within the session (starts at 1) |
agent_id | UUID | FK to the agents table |
agent_name | string | Denormalized agent name — queryable without a join |
tool_name | string | The tool that was called |
tool_parameters | JSONB | Full parameters at time of call |
decision | string | allow, deny, or review |
decision_reason | text | Why this decision was made |
policy_id | UUID | FK to the policy that fired; null when no policy matched |
policy_name | string | Denormalized policy name — queryable without a join |
workflow | string | The business-process workflow the call belongs to, as reported by the SDK adapter. Context for grouping the trail, not a binding axis |
duration_ms | integer | AIControl gateway latency in milliseconds |
created_at | timestamp | Server-set, immutable |
tool_response | text (truncated to 200 chars) | Reserved for the raw tool response. Always null via the MCP gateway path — the gateway does not currently record it. |
tool_parameters is stored as JSONB — it supports indexed queries on specific parameter values.
policy_id and policy_name are null when the decision is allow with reason no_matching_policy. When a policy fires, both are populated.
The dashboard and GET /audit-events also return a nested policy object (name, scope, effect) alongside each event when one fired, and can group the log view by workflow or by session.
Decision reasons
decision_reason | Meaning |
|---|---|
no_matching_policy | No policy matched — the engine's default allow applies |
tool_not_approved_for_agent | The agent's approved-tools allowlist didn't include this tool. Checked before any policy is evaluated |
policy_matched:<policy_name> | A policy matched. decision (deny or review) and policy_name tell you the rest — there's no separate reason code per condition type |
evaluation_error:* | The policy engine failed to evaluate the call. Always resolves to deny |
Viewing the audit log
Open the Audit Log tab in the React dashboard at http://localhost:3000. You can filter by agent, decision, and tool name. Each row shows the session, tool, decision, reason, policy, and latency.
For programmatic access, query the audit_events table directly:
-- All deny events in the last 7 days
SELECT
created_at,
agent_name,
tool_name,
tool_parameters,
decision_reason,
policy_name
FROM audit_events
WHERE decision = 'deny'
AND created_at > now() - interval '7 days'
ORDER BY created_at DESC;-- All events for a specific session
SELECT
sequence_number,
tool_name,
tool_parameters,
decision,
decision_reason,
duration_ms
FROM audit_events
WHERE session_id = 'b3e1f9a2-...'
ORDER BY sequence_number;-- Deny events by policy, for SOC2 compliance export
SELECT
ae.created_at,
ae.agent_name,
ae.tool_name,
ae.tool_parameters,
ae.decision_reason,
p.name AS policy_name,
p.compliance_frameworks
FROM audit_events ae
JOIN policies p ON ae.policy_id = p.id
WHERE ae.decision = 'deny'
AND 'SOC2' = ANY(p.compliance_frameworks)
ORDER BY ae.created_at DESC;What is and isn't stored
Stored in every audit event:
- Full tool parameters as submitted by the agent (JSONB)
- The policy that fired, with its compliance framework tags
- Decision and reason
- Intercept latency
Not stored:
- Tool execution results — AIControl governs the call, not the response
- LLM prompt or completion content — AIControl operates at the tool layer only
- Agent-to-agent communication — only tool calls that pass through the gateway are recorded
Immutability
Audit records are append-only. The API and dashboard have no delete or update endpoint for audit_events. The created_at timestamp is server-set and not modifiable by clients.
If you need to enforce immutability at the database level, configure your PostgreSQL user to have INSERT-only access to audit_events and remove UPDATE/DELETE grants.
Retention
AIControl runs a scheduled background purge that deletes audit_events (and any hitl_reviews referencing them) once they're older than your plan's retention window:
| Plan | Retention window |
|---|---|
| Community | 7 days |
| Business | 30 days |
| Enterprise | 90 days |
| Trial | 90 days |
The purge runs on a fixed interval (retention_purge_interval_hours, default 24 hours) — it is not manual and is not something you need to schedule yourself. This is the one sanctioned exception to audit_events being otherwise append-only: no other code path deletes or updates a row in it.
For regulated industries, most customers retain audit events for the duration required by their applicable framework (SOC2: 1 year, GDPR: varies by purpose, HIPAA: 6 years) — all of these exceed AIControl's built-in retention windows, so customers subject to those frameworks must archive audit events elsewhere before they age out.
Backup
The audit log lives in the aicontrol PostgreSQL database. Back it up using standard PostgreSQL tooling:
# Dump the audit_events table
docker compose exec postgres pg_dump \
-U aicontrol \
-t audit_events \
aicontrol > audit_events_$(date +%Y%m%d).sqlFor production deployments, configure continuous WAL archiving or use a managed PostgreSQL service with automated backups.
CSV Export (Enterprise)
Enterprise installations can export filtered audit events as a CSV file. The export honours the same filters as the list endpoint.
Via the dashboard: Open the Agent activity page, apply any filters, and click Export CSV. The file downloads immediately.
Via the API:
GET /audit-events/export
Authorization: Bearer <human-token>Query parameters (all optional):
| Parameter | Description |
|---|---|
decision | Filter by allow, deny, or review |
agent_id | Filter by agent UUID |
tool_name | Filter by exact tool name |
date_from | ISO 8601 start date |
date_to | ISO 8601 end date |
Response: text/csv with Content-Disposition: attachment; filename=audit_events.csv
Columns: id, created_at, agent_name, tool_name, decision, decision_reason, policy_name, session_id, duration_ms
Maximum 10,000 rows per export. Returns 402 for community installations.
SIEM Export (Business)
Business and Enterprise installations can stream every audit event to an outbound SIEM target as it's written, instead of pulling a CSV on demand. Two target types are supported:
- OTLP — ships each event as an OTel GenAI semantic-convention log record to a collector at
target_url. - Webhook — POSTs each event as flat JSON to
target_url.
Delivery is checkpoint-on-success-only: a target that's unreachable or returns a non-2xx response doesn't advance its checkpoint, so the event is retried on the next dispatch rather than dropped. Configure targets via POST /audit-export-configs (admin only); see API Reference for the full schema. Returns 402 for community installations.
This is separate from CSV Export above — SIEM export is a continuous push of every event to your own collector, CSV export is an on-demand pull of a filtered set.
Human review records
When a tool call returns decision: review, a corresponding record is created in the hitl_reviews table. Review records link to their audit event via audit_event_id.
Review records are accessible via GET /reviews and GET /reviews/{id}. See API Reference for the full schema.