/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."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.
Related
Audit CLI
midplane audit tail / since / stats.HTTP endpoints
The pull endpoints an external indexer reads these from.