Skip to main content
Every query the agent sends produces a sequence of audit events, written before the query runs. The audit log is append-only: local SQLite (/data/audit.db) for self-host, indexed Postgres for Cloud. The row shape is identical on both. See the audit trail concept for the model and how to read it.

Event types

A single query emits two or three events, in a fixed order. A standalone fifth type records a policy hot-swap. The pipeline order is non-negotiable: parse → policy → audit (ATTEMPTED + DECIDED) → execute → audit (EXECUTED | FAILED). If the ATTEMPTED + DECIDED write fails, the query does not execute — no query reaches the database unaudited.

Common columns

Every audit row carries the same columns. payload is JSON whose shape is keyed by event_type.
string (ULID)
Primary key. Sortable by creation time.
string (ULID)
Groups all events of one query. For POLICY_RELOADED, a synthetic ULID.
string
__self_host__ for OSS; the customer ULID for Cloud.
string
The database the event applied to — a databases[] name, or __default__ for the legacy single-database path.
string | null
The MCP clientInfo.name (e.g. claude-code). null for non-MCP callers and on POLICY_RELOADED.
string | null
The MCP clientInfo.version (e.g. 0.42.1). null for non-MCP callers and on POLICY_RELOADED.
string | null
The per-call task description from the query tool’s required intent argument (≤ 500 chars). null on the schema-browsing tools (list_tables, describe_table) and on POLICY_RELOADED.
string | null
Cloud-injected ULID identifying the MCP token that opened the session (from the X-Midplane-Token-Id header at MCP initialize). null on self-host, on a missing or malformed header, on non-MCP callers, and on POLICY_RELOADED. Added in 0.6.0 (additive-nullable).
integer
Milliseconds since the Unix epoch.
string
One of ATTEMPTED, DECIDED, EXECUTED, FAILED, POLICY_RELOADED.
JSON
Event-specific payload — see below.
integer
Row-shape version. Currently 3.

Payload shapes

ATTEMPTED

The intent record — written before policy, never redactable afterward.
string
The raw SQL, capped at 1 MiB (longer is denied as parse_error at the decision).
string (16 hex)
A stable fingerprint of the normalized AST — the same for “the same query intent” with different literals.

DECIDED

The policy decision. Always written; captures why on a denial. The optional dialect field (0.7.0+) names the dialect the query was parsed under — currently always postgres. Allowed:
"ALLOW"
The decision.
string
SELECT, INSERT, UPDATE, etc.
string[]
Schema-qualified, deduped table names (≤ 64).
string (optional)
The dialect the query was parsed under. Currently always postgres.
Denied:
"DENY"
The decision.
string
The rule that denied — table_access, multi_statement, dangerous_statement, or parse_error.
string
Human-readable, surfaced to the agent.
string (optional)
Present when parsing succeeded.
string[] (optional)
Present when parsing succeeded.
string (optional)
The parse dialect.

EXECUTED

Written on a successful run. Captures timing and the side-effect summary.
integer
Database execution time, in milliseconds.
integer
Midplane-added latency (parse + policy + audit + net), in milliseconds.
integer (optional)
For INSERT / UPDATE / DELETE.
integer (optional)
For SELECT.
string[] (optional)
The schema.table.column names that column masking transformed — column names only, never masked values. Present when a mask applied; added in 0.12.0 (additive-nullable).
string (optional)
How masking was applied: source_rewrite (the query was rewritten to redact the column at the source) or post_exec (the earlier post-execution masker). Present when a mask applied; 0.14.0+.
boolean (optional)
true when the post-execution masker ran the query but withheld every row because it couldn’t safely mask the result set (the fail-closed column_masking reject). Only the post_exec path produces this; under source_rewrite a masking reject happens before execution and is written as a FAILED event instead. 0.12.0+.
string (optional)
Human-readable explanation of why the result couldn’t be masked, surfaced to the agent. Present alongside masking_rejected; 0.12.0+.

FAILED

Written when an allowed query was rejected by the database.
integer
Time spent before the error, in milliseconds.
integer
Midplane-added latency, in milliseconds.
string
The database SQLSTATE class (e.g. 42P01), or column_masking when a source-rewrite mask couldn’t be applied and the query was denied before it ran (0.14.0+).
string
The error message, truncated at 4096 characters.
string (optional)
On a column_masking failure, which stage of the rewrite failed closed: gate, salt, shadow, or rewrite. 0.14.0+.

POLICY_RELOADED

Written when the in-memory policy is hot-swapped via POST /admin/policy. Not tied to a query; agent_name, agent_version, agent_intent, and mcp_token_id are always null.
string
Where the swap came from — admin_endpoint today.
object | null
The new table_access ({ default, tables }), or null when the swap didn’t carry one.

Indexes

The local SQLite table is indexed for the common read patterns: The table runs in WAL mode (journal_mode = WAL, synchronous = NORMAL) so reads don’t block the single audit writer.

schema_version

The current schema_version is 3. New fields are added additively — nullable, and ignored by older readers — without bumping schema_version, so an indexer pinned to 3 keeps working as fields are added.

Audit CLI

midplane audit tail / since / stats.

HTTP endpoints

The pull endpoints an external indexer reads these from.