Audit trail
What the platform records about every attempted action, and how to read it.
The audit trail is not a separate logging system. It is a direct readout of three platform tables that are populated by the mutation pipeline as a side effect of normal operation.
The three tables
| Table | Populated when | What it carries |
|---|---|---|
ActionInvocation | Every invokeAction call | actor, parameters, status (pending → terminal), correlation/causation, error message |
PolicyEvaluation | Every policy that runs during an invocation | policy ID, version, kind, result, reason, full dispatchEvidence |
AssetEvent | Every domain event emitted by a handler/saga | typed payload, subjectType/subjectId, sequence, actor info, actionInvocationId link |
Every action is one ActionInvocation row + zero-or-more PolicyEvaluation rows + zero-or-more AssetEvent rows. They share correlationId so a single SQL WHERE correlationId = … reconstructs everything.
What you can answer
| Question | Query |
|---|---|
| Who took which actions in this tenant in the last 30 days? | ActionInvocation filtered by tenantId, createdAt |
| Why was this invocation blocked? | PolicyEvaluation WHERE actionInvocationId = … |
| What was the parameter shape of an action 6 months ago? | ActionInvocation.parameters is preserved verbatim |
| What events did this saga produce? | AssetEvent WHERE correlationId = … |
| Did agents take any actions in the last week? | ActionInvocation WHERE actorType = 'agent' |
| Did anyone bypass policy X? | They couldn't — but you can confirm by joining PolicyEvaluation |
What auditors get for free
Because every mutation flows through the pipeline:
- No "shadow writes." Boundary tests (
packages/api/modules/*/boundary.test.ts) reject direct ORM mutations from API or UI code. There is no second path. - Failed attempts are persisted. A
validation_failed,blocked_by_policy, orfailedinvocation creates an audit row. Discarded attempts do not exist. - Decisions are inspectable.
dispatchEvidencerecords exactly which conditions evaluated, which evaluator ran, and which fallback triggered. "Why" is structured, not narrative. - Causality is preserved. Sagas link child invocations via
causationId. A reviewer can walk the tree from a top-level user action to every downstream side effect. - Time travel. The
AssetEventlog replays. State at any historical timestamp is reconstructible.
Evidence packets
For deeper compliance use cases, the platform's evidence package (packages/platform/evidence/index.ts) defines an EvidencePacketEnvelope:
interface EvidencePacketEnvelope<TPayload> {
id: string; // cep_<ULID>
tenantId: string;
spaceId: string;
packetType: string; // e.g. "credit_pull_consent_packet"
status: "draft" | "sealed" | "superseded";
subjects: EvidencePacketSubject[];
references: EvidencePacketReference[]; // → action_invocation, asset_event, etc.
payload: TPayload;
generatedAt: Date | string;
generatedBy: string;
policyVersion?: string;
checksum?: string;
correlationId: string;
causationId?: string;
}A packet is a curated bundle of references the platform can hand to a regulator: "here is the consent record, the credit pull invocation, the policy evaluations that allowed it, and the resulting events." Packets are produced by registered evidenceRenderers declared on a FabricModule.
Packets have status — draft (under construction), sealed (immutable, the official record), superseded (replaced by a newer sealed packet, retained).
Query patterns
-- Reconstruct everything that happened in one user request
SELECT *
FROM action_invocation, policy_evaluation, asset_event
WHERE correlation_id = $1
ORDER BY created_at;
-- All blocked attempts for a tenant
SELECT ai.*, pe.policy_id, pe.reason
FROM action_invocation ai
JOIN policy_evaluation pe ON pe.action_invocation_id = ai.id
WHERE ai.tenant_id = $1 AND ai.status = 'blocked_by_policy';
-- Per-actor activity heatmap
SELECT actor_type, action_id, COUNT(*)
FROM action_invocation
WHERE tenant_id = $1 AND created_at >= NOW() - INTERVAL '30 days'
GROUP BY 1, 2;See also
- Compliance — privacy, redaction, retention.
- Events as evidence — the underlying data model.
- Action triggers — why the pipeline is the only mutation path.