basalt log prints the record of what an agent asked for and what BasaltDB did about it. Use it to check a run before you undo it, or to see why a write was refused. It is read-only.
Read one run
Section titled “Read one run”basalt log --agent demo_bot --run RUN_ID
It prints one line per action: when, its status, the operation, the table, the number of rows, how long it took, the change id and the run’s reason. The rows an action wrote are indented below it.
PII: 0 columns classified, 10 unclassified; dictionary of 0 values, last scan 2026-11-15T07:20:32Z
basalt: warning: 10 columns the agent can read are not classified yet: basalt_demo.lead_notes.body, basalt_demo.lead_notes.id, ...; run basalt classify
2026-11-15T07:20:47Z REFUSED OTHER 0 rows 0ms 0b8c7d6e-5f4a-7b3c-8d2e-1f0a9b8c7d6e "Clean up the leads table"
refused: STATEMENT_REFUSED DDL_NOT_ALLOWED: DropStmt
2026-11-15T07:20:47Z APPLIED UPDATE basalt_demo.leads 3 rows 9ms 7c3e4f0a-5b1d-4c8e-9a62-0d1f2e3a4b5c "Qualify leads 1 to 3"
UPDATE basalt_demo.leads (blind write) {"id":1} score 37 -> 47, status NEW -> QUALIFIED
UPDATE basalt_demo.leads (blind write) {"id":2} score 74 -> 84, status NEW -> QUALIFIED
UPDATE basalt_demo.leads (blind write) {"id":3} score 11 -> 21, status NEW -> QUALIFIED
The first two lines are not part of the record. The PII: line counts the columns the agent can read that have a class, and the warning lists the ones that do not. On a fresh install every column is unclassified, so you see both. Run basalt classify to classify them. The warning is shortened here.
A row shows its table and key as JSON, and only the columns that changed, as column old -> new. An insert or a delete shows the key only. A refused action has an indented refused: line with its code and reason. A write refused for a missing intent is not recorded, so it is not listed.
Some rows carry a marker:
| Marker | What it means |
|---|---|
(read at xmin:N) |
The agent read this row before it wrote it. N is the row version it saw. |
(blind write) |
The agent never read this row in the run. A write like that is allowed, and recorded. |
(cascaded) |
The row changed because another row was deleted, not because the statement named it. |
values hidden |
The agent cannot read this table, so the record shows no values. |
values pruned |
The change is more than 30 days old. The record stays, the old and new values are gone. |
Consecutive reads collapse into one line, such as 3 reads, 42 rows: leads (2), lead_notes (1).
Read the whole record
Section titled “Read the whole record”Leave out --run to see the agent’s changes, newest first. Each change is one line with its reason.
basalt log --agent demo_bot
basalt log --agent demo_bot --sort id
--sort id puts the oldest first. The default is -id, newest first. Any other value is a usage error.
Other flags
Section titled “Other flags”| Flag | What it does |
|---|---|
--full |
Prints whole before and after rows, and every read on its own line. |
--json |
Prints the raw change records, for a script or an agent. See Change record. |
--full-check |
Checks the whole hash chain from the start. By default it checks from where it last checked. |
When a page of output has more to show, the next cursor is printed on stderr, so --json output stays clean.
What it warns about
Section titled “What it warns about”basalt log prints warnings on stderr, before or after the record. Each one names what to do.
- The hash chain of the ledger is broken.
- Tables the agent can write are not guarded.
- The engine has sent no heartbeat for 7 days, or came back after more than 7 days of silence.
- No passkey is active, or a passkey has not signed anything for 90 days. The warning names the passkey and says how to remove it with
basalt passkey remove. See Passkeys. - Row-level security hides every row of a table from the agent, so the agent reads 0 rows. See Hosts.
- Columns the agent can read are not classified yet. Run
basalt classify. See the CLI reference.
Which agent, and how it reads
Section titled “Which agent, and how it reads”--agent NAME picks the agent. If the engine runs as a background service, basalt log asks the engine for the record, so you do not need the agent’s credential file. If the engine is stopped, the command says so and prints the command to start it.
Without a service, the command reads the agent’s credential the way the engine does: --credential-file, then --agent NAME looked up in the engine config, then BASALT_DATABASE_URL. A flag always wins over the environment.
Your agent can read it too
Section titled “Your agent can read it too”An agent reads its own record with run_history and list_runs. It sees the same changes, with the same reasons. See MCP tools.
- The ledger: what each record holds and how long it is kept.
- Undo a run: put a run’s rows back.
- When something is refused: read a refusal and decide what to do.
BasaltDB · Apache-2.0