# See what it did

Read what an agent did, run by run, with basalt log. Reasons, rows, old and new values, and what was refused.

`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

```sh
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.

```text
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`](/reference/cli/#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

Leave out `--run` to see the agent's changes, newest first. Each change is one line with its reason.

```sh
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

| 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](/reference/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

`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](/guides/passkeys/).
- Row-level security hides every row of a table from the agent, so the agent reads 0 rows. See [Hosts](/guides/hosts/).
- Columns the agent can read are not classified yet. Run `basalt classify`. See the [CLI reference](/reference/cli/).

## 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

An agent reads its own record with `run_history` and `list_runs`. It sees the same changes, with the same reasons. See [MCP tools](/reference/mcp-tools/).

## Next

- [The ledger](/concepts/ledger/): what each record holds and how long it is kept.
- [Undo a run](/guides/undo/): put a run's rows back.
- [When something is refused](/guides/refused/): read a refusal and decide what to do.
