# Undo

How undo works when rows changed after the agent. Your team or your app wins, and undo lists what it left alone.

Undo puts back what an agent changed. It never overwrites what someone else changed since.

## People win

BasaltDB restores a row only if nobody has written it after the agent's last change. If your team or your app changed since, the row stays as it is. Undo skips it, lists it, and restores the rest.

![Undo restores the agent's rows and keeps rows changed after the agent: leads 1 and 3 get their old values back, lead 2, changed by your app after the agent, is kept as it is.](https://docs.basaltvault.app/diagrams/conflict-resolution.png)

When you undo a job, BasaltDB puts back the agent's changes: updated rows get their old values and deleted rows come back. Rows your team or your app changed after the agent stay as they are, and BasaltDB lists them instead of overwriting them.

Take one job where the agent changed leads 1, 2 and 3. Your app then marked lead 2 as won. When you undo the job:

```text
restore    basalt_demo.leads {"id": 1}: score: 47 -> 37, status: "QUALIFIED" -> "NEW"
restore    basalt_demo.leads {"id": 3}: score: 21 -> 11, status: "QUALIFIED" -> "NEW"
skip       basalt_demo.leads {"id": 2}: CHANGED_AFTER
basalt: reverted: 2 rows restored, 1 left as they are
```

A partial undo is a normal result. The command exits with `0`, and every skipped row is named with its reason.

## Why a row is skipped

| Reason in the list | Why                                                                          |
| ------------------ | ---------------------------------------------------------------------------- |
| `CHANGED_AFTER`    | Someone wrote the row after the agent: your team, your app or another agent. |
| `ROW_GONE`         | The row was deleted since, or the guard cannot see it.                       |
| `KEY_TAKEN`        | Undo would put a row back, and a row with that key exists now.               |
| `ALREADY_REVERTED` | An undo of this change already restored it.                                  |
| `VALUES_PRUNED`    | The values to put back have been pruned.                                     |
| `SCHEMA_CHANGED`   | The table, a column of the old row or the primary key changed.               |
| `CASCADE`          | Putting the row back would change a row that is not part of the undo.        |
| `CONSTRAINT`       | The restoring write fails a constraint when BasaltDB tries it.               |

## What undo covers

You can undo one change, one run, or one task. `basalt revert` prepares the work of the agent you name, and you can widen it to other agents' changes in the task on the signing page. Rows an agent updated get their old values. Rows it deleted come back, including rows removed by a cascade. Rows it inserted are deleted. A row the agent inserted and then deleted is left out.

Row limits do not apply to an undo. A large undo is applied in batches.

## The 30-day window

BasaltDB keeps old values for 30 days. After that the record of the change stays, but the values are pruned, and undo skips those rows. The rest of the ledger is kept for a year. Undo does not reach outside your database either: an email already sent stays sent.

## Your signature

An undo changes your data, so a person has to sign it. You review the rows, then confirm with a passkey: Touch ID, a security key, or your phone. Postgres checks the signature before it restores anything.

- An agent can preview an undo and prepare one for you. It cannot sign.
- A prepared undo expires after 1 hour if it is not signed.
- The undo is recorded as a new change that names you. History is never rewritten, and you can undo an undo.

## Next

- [Undo a run](/guides/undo/): the steps, with `basalt revert`.
- [The ledger](/concepts/ledger/): where the old values are kept.
- [Limits](/limits/): what undo cannot do.
