# When something is refused

Find what your agent reported, such as refused, held, stale read, row limit, intent missing or timeout, and what the agent and you should do next.

Start here when your agent says a write did not go through. Find the line that matches what it told you, read what happened, and do the two things listed: what the agent should do, and what you should do.

Every message from BasaltDB carries a code. The codes link to [Errors](/reference/errors/), which lists them all. To see the engine, the guard and the passkey in one place, run:

```sh
basalt status --agent NAME
```

It prints each problem it finds, with the command that fixes it.

| What the agent gets                           | Code                                                          | Go to                             |
| --------------------------------------------- | ------------------------------------------------------------- | --------------------------------- |
| A refused statement                           | [`STATEMENT_REFUSED`](/reference/errors/#statement-refused)   | [Refused](#refused)               |
| "Held for your approval"                      | [`APPROVAL_REQUIRED`](/reference/errors/#approval-required)   | [Held](#held)                     |
| The row changed since it was read             | [`STALE_READ`](/reference/errors/#stale-read)                 | [Stale read](#stale-read)         |
| More than 50 rows in one action               | [`ROW_LIMIT_EXCEEDED`](/reference/errors/#row-limit-exceeded) | [Row limit](#row-limit)           |
| The write had no reason                       | [`INTENT_REQUIRED`](/reference/errors/#intent-required)       | [Intent missing](#intent-missing) |
| A read or an undo took too long               | [`QUERY_TIMEOUT`](/reference/errors/#query-timeout)           | [Timeout](#timeout)               |
| No permission, no connection, nothing to undo | Others                                                        | [Something else](#something-else) |

## Refused

```json
{ "code": "STATEMENT_REFUSED", "reason": "DDL_NOT_ALLOWED" }
```

The code is [`STATEMENT_REFUSED`](/reference/errors/#statement-refused). It carries a reason.

**What happened.** The statement is one an agent may not run. The rules refuse `CREATE`, `ALTER`, `DROP` and `TRUNCATE` (reason `DDL_NOT_ALLOWED`), several statements in one call, `COPY`, a `WITH` that changes data, and functions that are not on the allowed list. Nothing was changed. The refusal is in the ledger.

**What the agent should do.**

- `DDL_NOT_ALLOWED`: stop, do not retry, and ask you. It must not look for another way to get the same result.
- `NO_PRIMARY_KEY` and `NOT_ALLOWED`: stop. The table or statement is outside what BasaltDB lets an agent do.
- Any other reason (`PARSE_ERROR`, `MULTIPLE_STATEMENTS`, `WRONG_TOOL`, `STATEMENT_NOT_ALLOWED`, `SELECT_INTO`, `LOCKING_READ`, `MERGE_NOT_ALLOWED`, `DATA_MODIFYING_WITH`, `FUNCTION_NOT_ALLOWED`): rewrite the statement as one plain `SELECT`, `INSERT`, `UPDATE` or `DELETE` and send it to the right tool. Reads go to `query`. Writes go to `write`.
- `NOT_REPEATABLE`: the write contains a value that changes between transactions, such as `now()`. Pass it as a parameter.

**What you should do.** If you want the change, run it yourself with your own admin connection. Agents never change your tables' structure. If the agent only needed a table it cannot touch, see [Something else](#something-else).

## Held

```text
Held for your approval: Delete 1 row in public.leads + 2 cascaded rows (public.lead_notes 2).
```

The code is [`APPROVAL_REQUIRED`](/reference/errors/#approval-required).

**What happened.** The write is allowed but not yet. The rules hold a write of 11 to 50 rows, and any `DELETE`, until a person approves it. The change has not run. A preview of the exact rows was made and rolled back.

**What the agent should do.** Show you the message as it is, with the link on its own line. Then repeat the same write with the approval id. That call waits up to 90 seconds for you. If the wait runs out, it does not wait again: it ends its turn and says the write is waiting. It must not split the write into smaller ones to avoid the hold.

**What you should do.** Open the link, check the rows and approve. The waiting write then applies by itself. If the agent already ended its turn, tell it you approved. If you lost the link, run:

```sh
basalt approve --agent NAME --open APPROVAL_ID
```

To refuse the write, close the page. The write stays blocked and expires after 24 hours. How approving works: [Approvals](/concepts/approvals/).

Related codes:

- [`APPROVAL_EXPIRED`](/reference/errors/#approval-expired): nobody approved within 24 hours. The agent asks you whether to send the write again.
- [`APPROVAL_INVALID`](/reference/errors/#approval-invalid) has a reason:
  - `NOT_SIGNED`, `BAD_SIGNATURE`: you have not signed it, or the signature did not verify. Open the link and approve again. The agent ends its turn.
  - `PASSKEY_REVOKED`: the passkey that signed was removed. Run `basalt passkey list --agent NAME`, add a passkey with `basalt init --agent NAME --add-passkey`, then open the link.
  - `ROWS_CHANGED`: the rows changed since you looked. The agent reads them again and sends a new write, which is held anew.
  - `ALREADY_USED`: the write already applied. The agent checks `run_history` and stops.
  - `NOT_THIS_AGENT`: the approval belongs to another agent. The agent calls `list_pending` for its own.
  - `DIFFERENT_WRITE`: the write is not the one you approved. The agent sends the held write unchanged.
- `NO_PASSKEY` (inside `APPROVAL_REQUIRED`): no passkey is registered, so nothing can be approved. Run `basalt init --agent NAME --add-passkey`. See [Passkeys](/guides/passkeys/).

## Stale read

```text
STALE_READ: public.leads {"id": 1} read at xmin ..., now ...
```

The code is [`STALE_READ`](/reference/errors/#stale-read).

**What happened.** The agent read a row, then tried to update or delete it. The row changed in between. Your team, your app or another agent wrote it. BasaltDB refuses the write, because the agent decided from values that are no longer true. A row the agent never read in the run is a blind write, which is allowed.

**What the agent should do.** Read the row again with `query`, then decide again and send a new write from the fresh values.

**What you should do.** Usually nothing. If it keeps happening on the same rows, someone or something else is changing them while the agent works. Pause that writer, or tell the agent to do the job in a quiet moment.

## Row limit

```json
{ "code": "ROW_LIMIT_EXCEEDED" }
```

The code is [`ROW_LIMIT_EXCEEDED`](/reference/errors/#row-limit-exceeded).

**What happened.** One action touched more than 50 rows, cascaded rows included. A delete of 5 rows that cascades into 50 more counts as 55. The limit is checked inside your Postgres. Nothing was changed.

**What the agent should do.** Stop and ask you. Only if you say yes, split the write into batches of up to 50 rows. Each batch is held, and you approve each one. It must not split the write on its own.

**What you should do.** Decide. A change of this size usually belongs in a migration or a data job you run yourself. If the agent's job really is this big, say yes to batches and review each one. Either way you can [undo the run](/guides/undo/) afterwards.

## Intent missing

```json
{ "code": "INTENT_REQUIRED" }
```

The code is [`INTENT_REQUIRED`](/reference/errors/#intent-required).

**What happened.** Every write needs a reason, and this one had none. Nothing was changed.

**What the agent should do.** Send the same write again with an `intent`: one sentence a person can read next to the rows it changed. A run started with `run_start` has its own reason, and each write still needs its own.

**What you should do.** If the agent forgets this often, tell it in its instructions to start a run with a reason and to give an intent on every write. If you see `INTENT_REQUIRED` from `psql` with the agent's own credential, that is the guard in your Postgres doing its job: a write that skips the engine has no intent. See [Runs](/concepts/runs/).

## Timeout

```text
{ "code": "QUERY_TIMEOUT", "reason": "TIME_LIMIT" }
```

The code is [`QUERY_TIMEOUT`](/reference/errors/#query-timeout). It carries a reason, `ROW_LIMIT` or `TIME_LIMIT`.

**What happened.** A read returned too many rows, or ran too long, and was stopped. Nothing was changed.

**What the agent should do.** Narrow the query. Add a `WHERE`, a `LIMIT`, or fewer columns, then send it again.

There is a second case. When an agent prepares an undo and that takes longer than 100 seconds, it gets `QUERY_TIMEOUT` with the reason `TIME_LIMIT`. Then it stops and tells you. It does not retry, and it ends its turn.

**What you should do.** For a timed-out undo, run the undo yourself. Preview it, then open it:

```sh
basalt revert --agent NAME --run RUN_ID --dry-run
basalt revert --agent NAME --run RUN_ID --open
```

Approve it in the browser. The command prints the revert id. Tell the agent that id, so it can carry on. See [Undo a run](/guides/undo/). A read stops at the engine's time limit, `limits.statementTimeoutMs`, which is 5 seconds by default. A preview of an undo (`preview_revert`) runs under the agent role's own limit, which you set with `--statement-timeout` when you run `basalt init` (30 seconds by default). Preparing an undo from `request_revert` may take 100 seconds, and from `basalt revert` up to 10 minutes. See [Reads and time](/reference/limits/#reads-and-time) and [Configuration](/reference/config/#limits).

## Something else

| Code                                                        | What happened                                                                             | What to do                                                                                                                                                                                          |
| ----------------------------------------------------------- | ----------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`PERMISSION_DENIED`](/reference/errors/#permission-denied) | The agent's role cannot touch that table.                                                 | The agent stops and asks you. If you want it to use the table, run `basalt init` again with the table added to `--write` or `--read`. Init keeps the agent's password.                              |
| [`UNAVAILABLE`](/reference/errors/#unavailable)             | The engine, the database or the guard cannot be reached.                                  | Run `basalt status --agent NAME`. It names the cause and the command. See below.                                                                                                                    |
| [`SQL_ERROR`](/reference/errors/#sql-error)                 | Postgres refused the statement: a syntax error, an unknown column or a constraint.        | The agent reads the message and `sqlState`, fixes the statement and sends it again. For `NO_ROWS_NOW` it queries the row first.                                                                     |
| [`INVALID_ARGUMENT`](/reference/errors/#invalid-argument)   | The call had a wrong or unknown argument.                                                 | The agent fixes the call. For `STALE_CURSOR` it repeats the call without `cursor`. For `SCOPE_FILTER` it drops entries that are not its own work.                                                   |
| [`REVERT_INVALID`](/reference/errors/#revert-invalid)       | The undo is already applied, the signature failed, or its rows changed since the preview. | `ROWS_CHANGED`: the agent asks for the undo again. `BAD_SIGNATURE` and `PASSKEY_REVOKED`: open the undo link and approve again, after adding a passkey if one was removed. Others: the agent stops. |
| [`REVERT_EXPIRED`](/reference/errors/#revert-expired)       | Nobody approved the undo within 1 hour (a large plan: 24 hours after signing).            | The agent asks you. Run `basalt revert --agent NAME --run RUN_ID --open` to prepare it again.                                                                                                       |
| [`NOTHING_TO_REVERT`](/reference/errors/#nothing-to-revert) | An undo has nothing to restore.                                                           | Nothing. The rows were only read, or your team or your app changed all of them.                                                                                                                     |
| [`NOT_FOUND`](/reference/errors/#not-found)                 | The run, change or approval id is not this agent's, or does not exist.                    | The agent lists its runs with `list_runs` and uses an id from there.                                                                                                                                |
| [`INTERNAL`](/reference/errors/#internal)                   | A defect in the engine or the guard.                                                      | The agent stops. Send the error id to the project. See [Security](/security/).                                                                                                                      |

`UNAVAILABLE` has a reason:

- `ENGINE_UNREACHABLE`: the engine is not running. Start it. `basalt status --agent NAME` prints the command for your machine.
- `DATABASE_UNREACHABLE`: your Postgres cannot be reached. The agent tries again after 10 seconds, then asks you to check that the database is up.
- `CREDENTIAL_REJECTED`: the database refused the agent's password. Restart the engine. If that does not fix it, run `basalt init --agent NAME` again with `--rotate-password`, then restart the engine.
- `GUARD_MISSING`: the guard is not in this database. Run `basalt init` again with the flags you used the first time.
- `GUARD_OUTDATED`: the guard is older than this version of basalt. Run `basalt upgrade --agent NAME`. See [Upgrade](/guides/upgrade/).
- `SETUP_PENDING`, `SETUP_EXPIRED`: your bot set up its engine from your laptop and the setup has not run yet. See [A bot on its own computer](/guides/bot-own-computer/).

If the agent reads 0 rows from a table that has rows, row-level security is on for that table and no policy lets the agent see rows. The agent gets a warning, `RLS_HIDES_ROWS`. See [Hosts](/guides/hosts/#row-level-security).

On an engine your bot runs, the person-side commands that need your admin connection string run on your own computer, never on the bot's machine. The bot gives you a setup code instead.

## Next

- [Errors](/reference/errors/): every code, every reason.
- [For agents](/reference/for-agents/): how an agent should read these results.
- [Rules](/concepts/rules/): what is allowed, held and refused by default.
