Skip to Content
PlatformHow-to guidesExport the audit trail

How to export the audit trail

Use this guide when a reviewer needs to replay what agents proposed and what people decided over a period. You start with a program (or the organisation) and a date range; you end with a CSV or JSON file whose rows carry the audit envelope and a database-set sequence and hash, optionally a thread export with the provenance footer, and a verifier run that recomputes the hashes. Exporting a program needs program.audit.read; the organisation needs org.audit.read; a thread export needs program.threads.manage (with a reason) or ownership of the thread.

The export describes controls, not a certification: entry_seq and row_hash are set by a database trigger the application role cannot bypass, audit rows cannot be updated or deleted by the application, and every foreign key on them refuses deletion of the referenced user or program. The per-organisation hash chain that links entries is designed for R2; exports gain a prev_hash column then.

Choose the scope and range

Setup › Audit (/p/<slug>/setup/audit) shows the program’s rows with filters for action, actor kind and date. Choose Export, pick csv or json and a range. The web app calls GET /programs/{program_id}/audit/export?from=...&to=...&format=csv (AuditExportQuery). For every program at once use Organization › Audit › Export, which calls GET /orgs/current/audit/export with org.audit.read.

Read the envelope

Each row is an AuditEventOut:

ColumnMeaning
action, target_type, target_id, program_idwhat changed (finding.create, protocol.approve, thread.share, document.ingest, …)
actor_kind, actor_user_id, actor_agent_idhuman with the user, agent with agent:<agent_key>@<org_id>, or system
agent_run_id, interrupt_id, approved_by_user_idthe run, the gate and the person who approved an agent write
proposal_hash, approved_hashthe proposal as the agent made it and as the person approved it (they differ when the proposal was edited before approval)
signature_idthe signature of a signing mutation
reasonrequired for updates and deletions of records in review or approved states
occurred_at, created_atwhen it happened; created_at is set by the database
entry_seq, row_hashglobal sequence (gaps allowed, order preserved) and sha256(JCS(row without entry_seq and row_hash)), both set by the BEFORE INSERT trigger
diff, request_idbefore and after values, and the request that caused the row

Verify the hashes

The Python twin of the SQL function recomputes every hash:

cd api uv run python - <<'PY' import csv, sys from app.services.audit import verify_row_hash rows = list(csv.DictReader(open("audit-export.csv"))) bad = [r["entry_seq"] for r in rows if not verify_row_hash(r)] print(f"{len(rows)} rows, {len(bad)} hash mismatches", bad[:5]) PY

verify_row_hash canonicalises the row with RFC 8785 JSON exactly as the database’s trovensa_jcs does (tests compare the two byte for byte), so a mismatch means the row differs from what was inserted. entry_seq must be strictly increasing inside one organisation.

Export a thread

On a thread choose Export (owner) or Export with reason (program.threads.manage). GET .../threads/{thread_id}/export returns messages, artifacts (every version), tool calls, interrupts and the run snapshots, followed by the provenance footer {agent_id, agent_version_id, model_config_label, run_id, generated_at, citations[], statement: "agent-generated, unsigned"} and the attribution blocks of every cited knowledge source. customer_internal documents leave as locator and hash only; cc_by_nd passages are listed separately as quotations. The export is audited (thread.export).

Replay a run

Setup › Agents filters runs by agent_version_id. Open a run’s snapshot: chunk ids and hashes, record ids with updated_at, sop_version_ids, prompt version, tool allowlist, policy version, permission snapshot and the model check outcome. Together with the audit export a reviewer can answer which SOP version applied, which sources were read, which tool calls were proposed, approved, edited or denied, and who signed what with which meaning.

Troubleshooting

SymptomCauseFix
Export is emptythe range is outside the program’s rows, or the program was archived and filtered outwiden the range; archived programs keep their rows
A row has actor_kind = system and no userworker-side writes such as document.ingest, document.purge, thread.redactexpected
approved_hash differs from proposal_hashthe person edited the proposal before approvingthe record page shows “edited before approval” with the diff
A thread export contains “Content removed under the retention policy on <date>the thread passed thread_retention_monthsmessage bodies are tombstones; tool calls, interrupts, snapshots, signatures and audit rows remain
Last updated on