The read-only archive of finished runs, for auditors and anyone reconstructing what happened.
Routes: /records, /records/[processId]
APIs: /api/records, /api/records/[processId], /api/records/export
There is no records collection. Every record is computed from the process document on read:
"Derived from the process document; never stored. Deliberately small — the archive lists thousands of runs without shipping full context."
Nothing to sync, nothing to drift, no migration. The cost is that every read recomputes, and the list still loads every process — see storage.
All three routes require processes:audit, deliberately not processes:read_all:
const denied = await requirePermission(request, PERMISSIONS.PROCESSES_AUDIT, …);
read_all also confers write. An auditor should be able to read every run and change
none, so the read-only grant exists separately. See
permissions.
RecordSummary (platform/contracts/src/record/record-summary.ts) is deliberately small — it is
what the list ships per row.
| Field | |
|---|---|
outcome | in_progress | completed | failed |
checks | { total, checked, unchecked } over checkbox fields the run reached |
keyFields | verification values lifted out so a run is identifiable in a list |
participantIds | every non-system actor who wrote or completed, first-seen order |
automationIssueCount | deliveries that failed, or sent without reaching everyone |
reachedStepCount / stepCount | how far it got |
startedAt updatedAt closedAt |
ProcessStatus is only running | completed. RecordOutcome adds failed, derived
from error and result rather than read from the document. That is a workaround, not a
design — see known issues.
slack_notify and telegram_notify record their outcome into context and advance.
automationIssues() lifts those out:
"a signer notification that never landed leaves a clean-looking completed record. Surfacing it is the difference between 'the run completed' and 'everyone who should have been told, was'."
It prefers process.automationWarnings, which the engine now writes as deliveries
happen, and falls back to reading context for runs that finished before that existed.
Both paths go through lib/automation-outcome.ts, so records and the runner cannot
disagree about whether something sent.
Two kinds are reported: failed (nothing sent) and partial (sent, but a mentioned user
was not in the channel and so was never pinged).
uncheckedChecks() tallies checkbox fields on steps the run actually reached. The
audit-relevant number is unchecked — a verification box nobody ticked before the record
closed.
templateDivergence() compares the run's frozen snapshot against the live template and
reports only what changes how this record reads: steps added or removed, and fields
this run populated that were since removed or retitled.
Not a whole-object diff, on purpose:
"It fires on every cosmetic edit… so within weeks every record would carry the banner and nobody would read it."
⚠️ Both templates are expanded before comparison. Comparing an expanded snapshot against an unexpanded live template reports every subroutine-using template as diverged.
Surfaced as a banner on the detail page (data.template.divergence).
buildRecordTimeline() merges two sources into one ordered sequence:
stepContextAudit — field writessteps[].updatedUTC / updatedById — step completions{ at, kind: "process_started" | "field_update" | "step_completed" | "process_closed",
actorId, isSystem, stepKey?, stepTitle?, fields?, severity? }
isSystem distinguishes automation from people, via SYSTEM_STEP_CONTEXT_USER_ID.
severity: "error" marks an entry whose automation outcome reported failure.
The /records page exposes all five filters, which map straight onto RecordQuery:
| Param | |
|---|---|
from / to | inclusive ISO date or datetime; a date-only to covers the whole day |
status | all | in_progress | completed | failed |
templateKey | one template |
q | free text over process id, template, actors and key fields — a tx hash, a nonce, an operator |
Outcome is shown as a badge: Completed (green), In progress (blue), Failed (red). That red state is the one the platform itself cannot store.
GET /api/records/export returns a processos-record-bundle document for retention.
Records read; they never write. They consume process.template (the snapshot),
context, and stepContextAudit — see processes.
The related per-process audit dump lives at /api/process/{id}/audit — see
audit and export.