BasaltDB keeps one record per action an agent asks for. It is written whether the action was applied, held or refused. run_history and basalt log --json return these records. To read a run in plain text, see See what it did.
The shape
Section titled “The shape”{
"changeId": "0193a4c2-8a10-7c55-8e21-0b7a6d4c9e10",
"at": "2026-11-10T06:02:11Z",
"actor": {
"kind": "AGENT",
"name": "mbg-tracker-bot",
"runId": "0193a4c2-7d1e-7b3a-9f00-3c1d5e8a2b44",
"taskId": "0193a4c2-7d1e-7b3a-9f00-3c1d5e8a2b45"
},
"intent": "Merge duplicate case reports from yesterday",
"target": { "table": "public.cases", "key": { "id": 4211 } },
"op": "UPDATE",
"statement": "UPDATE cases SET status = $1 WHERE id = $2",
"before": { "status": "open" },
"after": { "status": "merged" },
"basedOn": { "version": "xmin:88123" },
"durationMs": 12,
"rows": 1,
"policy": { "decision": "ALLOW", "rule": "default.single-row-update" },
"status": "APPLIED"
}
Fields
Section titled “Fields”| Field | What it holds |
|---|---|
changeId |
The record’s id. A UUIDv7, so ids sort by time. |
at |
When it happened. RFC 3339 UTC. |
actor |
Who asked. kind is AGENT or PERSON. name, runId and taskId say which agent, run and task. |
intent |
The reason the agent gave, in a sentence. |
target |
The table and the primary key of the row, when the action touched one row. |
op |
What kind of statement. See the table below. |
statement |
The SQL the agent sent. Values are in params. |
before |
The row’s old values, for the columns that changed. |
after |
The row’s new values, for the columns that changed. |
basedOn |
The row version the agent last read, as xmin:<n>. See What the agent read. |
durationMs |
How long the statement took. |
rows |
Rows touched. For a read, rows returned. |
policy |
The decision and the rule behind it. decision is ALLOW, HOLD or REFUSE. |
status |
Where the action ended up. See the table below. |
As run_history and basalt log --json return it, a record also carries:
| Field | What it holds |
|---|---|
params |
The values bound to $1, $2, … |
cascadedRows |
How many of the rows changed through cascades. |
rowChanges |
One entry per row written, cascades included. Each has key, before and after, and redacted or pruned when the values are hidden. |
revertId |
On a revert’s own record: the revert it belongs to. |
revertedBy |
The revert changes that restored rows of this change. |
approvalId |
On an applied write that was held first: the approval that released it. |
reason |
On a REFUSED record: { "code", "message" } with an error code. |
Values
Section titled “Values”| Field | Values |
|---|---|
status |
APPLIED, APPROVAL_PENDING, REFUSED, APPROVAL_EXPIRED, REVERTED |
op |
SELECT, INSERT, UPDATE, DELETE, OTHER (a refused statement that is none of these), REVERT (a person’s undo) |
actor.kind |
AGENT, PERSON |
policy.decision |
ALLOW, HOLD, REFUSE |
What the statuses mean:
APPLIED: it ran. A write that was held first is replaced by anAPPLIEDrecord that carriesapprovalId. A write only partly undone staysAPPLIED.APPROVAL_PENDING: held, waiting for your approval. It has not run.REFUSED: the rules or the guard said no. Nothing changed.reasonsays why.APPROVAL_EXPIRED: a held write was not approved within 24 hours.REVERTED: an undo restored every row of this change. If any row was skipped, the status staysAPPLIEDandrevertedBylists the undo.
What the agent read
Section titled “What the agent read”Every query is a change record with op: SELECT and status: APPLIED. Its rows is the number of rows returned. It has its statement, parameters and duration, no rowChanges, and the run’s intent when it ran in a run. BasaltDB keeps the version of each row the read saw.
An UPDATE or DELETE of a row the run had read carries basedOn: { "version": "xmin:<n>", "changeId": "<the read>" } on that row’s entry in rowChanges. A row the run never read carries blindWrite: true. That is allowed. Cascaded rows and inserts carry neither. A change with exactly one row that was read also carries basedOn at the top level, as in the example. If the row changes after the read, the write is refused with STALE_READ.
An undo is a new record
Section titled “An undo is a new record”An undo restores old values and never edits or deletes ledger rows. It is recorded as one change with op: REVERT and actor.kind: PERSON, plus one entry in rowChanges per restored row. A skipped row is listed with its reason (CHANGED_AFTER, ROW_GONE, KEY_TAKEN, ALREADY_REVERTED, VALUES_PRUNED, SCHEMA_CHANGED, CASCADE or CONSTRAINT) and no values. See Undo.
How long it is kept
Section titled “How long it is kept”| What | How long |
|---|---|
before and after values |
30 days |
| The rest of the ledger: who, what, when | 1 year |
A row whose old values are older than 30 days shows pruned and cannot be restored. It is skipped as VALUES_PRUNED. The numbers are also in Limits and defaults.
Tamper evidence
Section titled “Tamper evidence”The ledger is append-only and hash-chained, inside your Postgres. basalt log checks the chain each time it runs and warns if it is broken. See Ledger.
BasaltDB · Apache-2.0