# Error codes

Every BasaltDB error code and its reasons, what each means, why it happens, what the agent should do and what you should do.

Every refusal, hold and failure from BasaltDB carries a code. Each code on this page has its own anchor, such as `/reference/errors/#stale-read`, so an error from the engine can link straight to its entry. If your agent just reported a problem and you want the short version, start with [When something is refused](/guides/refused/).

## What an error looks like

A tool error is an MCP tool result with `isError: true`. The text is the JSON body, with no sentence before it. This is an example:

```json
{
  "code": "STATEMENT_REFUSED",
  "message": "DDL_NOT_ALLOWED: DropStmt",
  "reason": "DDL_NOT_ALLOWED",
  "personMessage": "The agent tried to change the database schema (DDL). Basalt never lets an agent do that. Make the change yourself if you want it.",
  "nextSteps": [
    {
      "actor": "AGENT",
      "action": "STOP",
      "text": "Do not retry and do not look for another way to change the schema."
    },
    {
      "actor": "AGENT",
      "action": "ASK_PERSON",
      "text": "Tell the person which schema change you need; they make it themselves."
    }
  ],
  "docsUrl": "https://docs.basaltvault.app/reference/errors/#statement-refused"
}
```

| Field           | When it is there                                                                                                                                                   |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `code`          | Always. One of the codes below. Stable.                                                                                                                            |
| `message`       | Always. One line for the agent.                                                                                                                                    |
| `reason`        | For every code that has more than one case. Listed under each code.                                                                                                |
| `nextSteps`     | Always. At least one step, first step first. See [Next steps](#next-steps).                                                                                        |
| `personMessage` | When you must know or act. The agent shows it to you word for word.                                                                                                |
| `docsUrl`       | Always. The link to the code's section on this page.                                                                                                               |
| `sqlState`      | When Postgres raised the error. `SQL_ERROR` uses it to say which.                                                                                                  |
| `errorId`       | On `UNAVAILABLE` and `INTERNAL`. Quote it when you report a problem.                                                                                               |
| `approval`      | On `APPROVAL_REQUIRED`: `approvalId`, `expiresAt`, `rows`, `headline`, `approvalUrl`, `approveCommand` and `personMessage`. See [Approvals](/concepts/approvals/). |

A refused write is also a change record with status `REFUSED` and the same code and reason, so it shows in `basalt log`. The exception is `INTENT_REQUIRED`: a write with no intent leaves no record. See [Ledger](/concepts/ledger/) and [Change record](/reference/change-record/).

## Next steps

Each step in `nextSteps` has an `actor` (`AGENT` or `PERSON`), an `action` and one plain sentence of `text`. The engine fills in the arguments and commands, so none of them has a placeholder.

| Actor    | Action                  | What it means                                                                                                    |
| -------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `AGENT`  | `CALL_TOOL`             | Call `tool` with `arguments`. With `repeatCall: true` the arguments merge over the call that failed.             |
| `AGENT`  | `REVISE_CALL`           | Change the statement or arguments as `text` says, then call the tool again.                                      |
| `AGENT`  | `RETRY_SMALLER_BATCHES` | Split the write into writes of at most `maxRows` rows. Only after you said yes.                                  |
| `AGENT`  | `ASK_PERSON`            | Ask you a question and wait for the answer.                                                                      |
| `AGENT`  | `END_TURN`              | End the turn. You act next.                                                                                      |
| `AGENT`  | `STOP`                  | Do not retry. Do not work around it.                                                                             |
| `AGENT`  | `RUN_COMMAND`           | Run `command`. Only on an engine the agent operates. See [A bot on its own computer](/guides/bot-own-computer/). |
| `PERSON` | `OPEN_LINK`             | You open `url`. Often with a `command` that opens the same page.                                                 |
| `PERSON` | `RUN_COMMAND`           | You run `command`.                                                                                               |
| `PERSON` | `TELL_AGENT`            | You tell the agent that you did it.                                                                              |

The agent follows the `AGENT` steps and shows you each `PERSON` step's `command` and `url` unchanged. How an agent reads all of this: [For agents](/reference/for-agents/).

## All codes

| Code                                        | In short                                                 |
| ------------------------------------------- | -------------------------------------------------------- |
| [`INTENT_REQUIRED`](#intent-required)       | A write had no reason.                                   |
| [`STALE_READ`](#stale-read)                 | The row changed after the agent read it.                 |
| [`STATEMENT_REFUSED`](#statement-refused)   | The statement is one an agent may not run.               |
| [`ROW_LIMIT_EXCEEDED`](#row-limit-exceeded) | More than 50 rows in one action.                         |
| [`QUERY_TIMEOUT`](#query-timeout)           | A read or an undo ran out of time or rows.               |
| [`APPROVAL_REQUIRED`](#approval-required)   | The write is held for your approval.                     |
| [`APPROVAL_EXPIRED`](#approval-expired)     | Nobody approved within 24 hours.                         |
| [`APPROVAL_INVALID`](#approval-invalid)     | The approval cannot be used.                             |
| [`REVERT_INVALID`](#revert-invalid)         | The undo cannot be applied as asked.                     |
| [`REVERT_EXPIRED`](#revert-expired)         | The undo was not signed in time.                         |
| [`NOTHING_TO_REVERT`](#nothing-to-revert)   | The undo would restore nothing.                          |
| [`PERMISSION_DENIED`](#permission-denied)   | The agent's role cannot touch that table.                |
| [`NOT_FOUND`](#not-found)                   | The id is not this agent's, or does not exist.           |
| [`SQL_ERROR`](#sql-error)                   | Postgres refused the statement.                          |
| [`INVALID_ARGUMENT`](#invalid-argument)     | The call has a wrong or unknown argument.                |
| [`UNAVAILABLE`](#unavailable)               | The engine, the guard or the database cannot be reached. |
| [`INTERNAL`](#internal)                     | A defect in the engine or the guard.                     |

<a id="intent-required"></a>

## `INTENT_REQUIRED`

**What it means.** A write arrived without an intent. Nothing was changed.

**Why it happens.** Every write needs a reason a person can read next to the rows it changed. The engine checks it, and so does the guard in your Postgres (SQLSTATE `BS001`). A write with the agent's own credential that skips the engine fails there too.

**What the agent should do.** Send the same write again with an `intent`: one sentence, at most 500 characters. A run started with `run_start` has its own reason, and each write still needs its own.

**What you should do.** Usually nothing. If an agent forgets this often, tell it in its instructions to start a run with `run_start` and to give an `intent` on every write. See [Runs](/concepts/runs/) and [Intent missing](/guides/refused/#intent-missing).

<a id="stale-read"></a>

## `STALE_READ`

**What it means.** The agent read a row, then tried to update or delete it, and the row changed in between. Nothing was changed.

**Why it happens.** Your team, your app or another agent wrote the row after the agent read it, so the agent would decide from values that are no longer true. The guard compares the row's version (`xmin:<n>`) with the one in the run's reads (SQLSTATE `BS004`). A row the run never read is a blind write. That is allowed.

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

**What you should do.** Usually nothing. If it keeps happening on the same rows, something else is writing them while the agent works. Pause that writer, or run the job in a quiet moment. See [Stale read](/guides/refused/#stale-read).

<a id="statement-refused"></a>

## `STATEMENT_REFUSED`

**What it means.** The statement is one an agent may not run, or it went to the wrong tool. Nothing was changed. The refusal is in the ledger.

**Why it happens.** The engine's rules refuse DDL, several statements in one call, `COPY`, a `WITH` that changes data, functions that are not on the allowed list and other statements. The guard refuses the same things in Postgres (SQLSTATE `BS007`). Agents cannot drop or change tables. See [Rules](/concepts/rules/).

**Reasons.**

| Reason                  | What happened                                                                                   | What the agent does                                          |
| ----------------------- | ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------ |
| `PARSE_ERROR`           | The statement does not parse.                                                                   | Fix the SQL and send it again.                               |
| `MULTIPLE_STATEMENTS`   | More than one statement in one call.                                                            | Send one statement per call.                                 |
| `WRONG_TOOL`            | A write went to `query`, or a read went to `write`.                                             | Send it to the right tool.                                   |
| `DDL_NOT_ALLOWED`       | `CREATE`, `ALTER`, `DROP` or `TRUNCATE`.                                                        | Stop, do not retry, and ask you. You get a `personMessage`.  |
| `STATEMENT_NOT_ALLOWED` | `SET`, `DO`, `COPY`, `EXPLAIN`, transaction control or another utility statement.               | Rewrite as a plain `SELECT`, `INSERT`, `UPDATE` or `DELETE`. |
| `SELECT_INTO`           | `SELECT ... INTO`.                                                                              | Rewrite it as a plain `SELECT`.                              |
| `LOCKING_READ`          | `SELECT ... FOR UPDATE` and similar.                                                            | Drop the locking clause.                                     |
| `MERGE_NOT_ALLOWED`     | `MERGE`.                                                                                        | Use separate `INSERT` and `UPDATE` writes.                   |
| `DATA_MODIFYING_WITH`   | A `WITH` that inserts, updates or deletes.                                                      | Send the changes as separate writes.                         |
| `FUNCTION_NOT_ALLOWED`  | A function that is not on the allowed list.                                                     | Rewrite without that function.                               |
| `NOT_REPEATABLE`        | A write that would be held contains a value that changes between transactions, such as `now()`. | Pass the value as a parameter.                               |
| `NO_PRIMARY_KEY`        | The write targets a table whose rows the guard cannot identify.                                 | Stop. You get a `personMessage`.                             |
| `NOT_ALLOWED`           | The guard refused the statement for a reason outside the list above.                            | Stop.                                                        |

**What you should do.** If you want the change that was refused, run it yourself with your own admin connection. Agents never change your tables' structure. For `DDL_NOT_ALLOWED` the agent asks you. See [Refused](/guides/refused/#refused).

<a id="row-limit-exceeded"></a>

## `ROW_LIMIT_EXCEEDED`

**What it means.** One action touched more than 50 rows, cascaded rows included. Nothing was changed.

**Why it happens.** A delete of 5 rows that cascades into 50 more counts as 55. The limit is checked in your Postgres (SQLSTATE `BS003`). It applies to a direct connection with the agent's credential too.

**What the agent should do.** Stop and ask you. Only if you say yes, split the write into writes of up to 50 rows, with the same intent and run. Each batch is held and needs your approval. It must not split the write on its own.

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

<a id="query-timeout"></a>

## `QUERY_TIMEOUT`

**What it means.** A read hit the row limit or the time limit and was stopped. Nothing was changed.

**Why it happens.** A `SELECT` returned more rows than the read row limit, or ran past the engine's time limit (SQLSTATE `57014`). The same code covers `request_revert` when preparing an undo runs past its 100-second limit.

**Reasons.**

| Reason       | What happened                                         | What the agent does                                                           |
| ------------ | ----------------------------------------------------- | ----------------------------------------------------------------------------- |
| `ROW_LIMIT`  | The read returned too many rows.                      | Add a `WHERE` or a `LIMIT`, or select fewer columns, and send it again.       |
| `TIME_LIMIT` | The read ran too long.                                | Narrow the query and send it again.                                           |
| `TIME_LIMIT` | `preview_revert` or `request_revert` ran out of time. | Stop, tell you, and end the turn. You run the revert from a terminal (below). |

**What you should do.** For a read, nothing. For a timed-out undo, run it 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. The limits are in [Limits and defaults](/reference/limits/). See [Timeout](/guides/refused/#timeout).

<a id="approval-required"></a>

## `APPROVAL_REQUIRED`

**What it means.** The write is allowed but not yet. It is held for your approval and has not run.

**Why it happens.** The rules hold a write of 11 to 50 rows, and any `DELETE`, until a person approves it with a passkey (SQLSTATE `BS002`). The engine previews the exact rows in a transaction it rolls back, records the change as `APPROVAL_PENDING` and returns the approval. The agent gets `approval` with an id, an expiry (24 hours), a one-line `headline`, a link and a command.

**Reasons.**

| Reason          | What happened                                              | Steps                                                                                                    |
| --------------- | ---------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| `HELD`          | The write is held and the engine serves the approval page. | You open the link. The agent repeats the write once with `approvalId`, which waits up to 90 seconds.     |
| `HELD_NO_PAGES` | The engine does not serve the approval pages.              | You run `basalt approve --agent NAME --open APPROVAL_ID`. The agent then repeats the write.              |
| `NO_PASSKEY`    | No passkey is registered, so nothing can be approved.      | You run `basalt init --agent NAME --add-passkey` and open the link. The agent ends its turn.             |
| `WAIT_ENDED`    | The agent waited and you had not approved yet.             | You open the link and tell the agent when you approved. The agent ends its turn. It does not wait again. |

**What the agent should do.** Show you `personMessage` word for word, with the link on its own line. Then, in the same turn, repeat the same `sql`, `params`, `intent` and `runId` with `approvalId`. That call waits for you. If the wait ends, end the turn and say the write is waiting. Never split the write to get under the hold.

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

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

To refuse, close the page. The write stays blocked and expires after 24 hours. How it works: [Approvals](/concepts/approvals/). See [Held](/guides/refused/#held) and [Passkeys](/guides/passkeys/).

<a id="approval-expired"></a>

## `APPROVAL_EXPIRED`

**What it means.** Nobody approved the held write within 24 hours. It is blocked and nothing was changed.

**Why it happens.** A hold lasts 24 hours. No answer means blocked (SQLSTATE `BS005`). The ledger keeps the held and expired rows.

**What the agent should do.** Ask you whether to send the write again. If you say yes, send it as a new write, which is held anew.

**What you should do.** Tell the agent yes or no. You get a `personMessage` that says what expired.

<a id="approval-invalid"></a>

## `APPROVAL_INVALID`

**What it means.** The approval cannot be used for this call. Nothing was changed.

**Why it happens.** The approval does not exist, was already used, belongs to another agent, is not signed yet, or no longer matches the rows (SQLSTATE `BS005`). Only the guard in Postgres decides this, from your passkey signature.

**Reasons.**

| Reason            | What happened                                               | What the agent does                                                          | What you do                                                                                            |
| ----------------- | ----------------------------------------------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| `NOT_SIGNED`      | The approval is not signed yet.                             | End the turn.                                                                | Open the link and approve.                                                                             |
| `ALREADY_USED`    | The write already applied.                                  | Check `run_history`, then stop.                                              | Nothing.                                                                                               |
| `DIFFERENT_WRITE` | The statement, parameters or run differ from what was held. | Send the held write unchanged, with `approvalId`.                            | Nothing.                                                                                               |
| `NOT_THIS_AGENT`  | The approval belongs to another agent.                      | Call `list_pending` for its own.                                             | Nothing.                                                                                               |
| `ROWS_CHANGED`    | The rows changed after you looked.                          | Read the rows again with `query`, then send a new write, which is held anew. | Approve the new hold. You get a `personMessage`.                                                       |
| `BAD_SIGNATURE`   | The signature did not verify for this approval.             | End the turn.                                                                | Open the link and approve again.                                                                       |
| `PASSKEY_REVOKED` | The passkey that signed was removed or is not registered.   | End the turn.                                                                | `basalt passkey list --agent NAME`, then `basalt init --agent NAME --add-passkey`, then open the link. |

**What you should do.** See the table. More: [Held](/guides/refused/#held) and [Passkeys](/guides/passkeys/).

<a id="revert-invalid"></a>

## `REVERT_INVALID`

**What it means.** The undo cannot be applied as asked. Nothing was restored.

**Why it happens.** The revert does not exist, was already applied, was rejected, carries a bad signature, or its rows changed since the preview (SQLSTATE `BS005`). For a large plan applied in batches: the plan is not this agent's, is not signed, is already finished, or a line is not in the plan, is repeated or is already applied.

**Reasons.**

| Reason            | What happened                                                 | What the agent does                                                 | What you do                                                                                            |
| ----------------- | ------------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| `LINE_INVALID`    | A batch line is not in the plan, repeated or already applied. | Call `revert_plan` with `pending: true` and use the lines it lists. | Nothing.                                                                                               |
| `PLAN_NOT_SIGNED` | The large plan is not signed yet.                             | Call `revert_plan`, then end the turn.                              | Open the revert link and approve.                                                                      |
| `PLAN_FINISHED`   | The plan is already signed, or already finished.              | Call `revert_plan` to confirm, then stop.                           | Nothing.                                                                                               |
| `NOT_THIS_AGENT`  | The revert belongs to another agent.                          | Stop.                                                               | Nothing.                                                                                               |
| `ROWS_CHANGED`    | Rows changed since the preview.                               | Ask for the undo again with `request_revert`, same scope.           | Approve the new undo. You get a `personMessage`.                                                       |
| `BAD_SIGNATURE`   | The signature did not verify for this revert.                 | End the turn.                                                       | Open the revert link and approve again.                                                                |
| `PASSKEY_REVOKED` | The passkey that signed was removed or is not registered.     | End the turn.                                                       | `basalt passkey list --agent NAME`, then `basalt init --agent NAME --add-passkey`, then open the link. |

A revert you rejected on the page is also `REVERT_INVALID`. See [Undo a run](/guides/undo/).

<a id="revert-expired"></a>

## `REVERT_EXPIRED`

**What it means.** The undo was not signed in time. Nothing was restored, or, for a large plan, only what was applied before the deadline.

**Why it happens.** A prepared undo must be signed within 1 hour. A signed large plan must be finished within 24 hours of the signature (SQLSTATE `BS005`).

**Reasons.** `NOT_SIGNED` (nobody signed within the hour) and `NOT_FINISHED` (a signed large plan was not finished in 24 hours).

**What the agent should do.** Ask you whether to prepare it again.

**What you should do.** Run the undo again and approve it:

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

<a id="nothing-to-revert"></a>

## `NOTHING_TO_REVERT`

**What it means.** An undo would restore nothing.

**Why it happens.** Every row in the scope would be skipped, or the scope wrote no rows (SQLSTATE `BS009`). Rows are skipped when your team or your app changed them since the agent did. People win.

**Reasons.**

| Reason                  | What happened                                                                                   | What the agent does                                                                        |
| ----------------------- | ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| `READS_ONLY`            | The scope wrote no row an undo could restore: only reads, or rows it both inserted and deleted. | Stop. You get a `personMessage`.                                                           |
| `ALL_CHANGED_BY_PEOPLE` | Every row would be skipped because it changed after the agent.                                  | Stop. You get a `personMessage`.                                                           |
| `ALREADY_REVERTED`      | Every row was last written by a revert of this scope.                                           | Ask you whether to undo that undo. If yes, `request_revert` with that revert's `changeId`. |

**What you should do.** Usually nothing. Read the `personMessage`. For `ALREADY_REVERTED` you can say yes to bring the agent's values back. You still approve it.

<a id="permission-denied"></a>

## `PERMISSION_DENIED`

**What it means.** The agent's role has no privilege on that table. Nothing was changed.

**Why it happens.** Postgres refused the statement with SQLSTATE `42501`. The agent can read and write only the tables you gave it with `--write` and `--read`.

**What the agent should do.** Stop and ask you whether it may use the table. Do not look for another route to the data.

**What you should do.** If you want the agent to use the table, run `basalt init` again with the table added to `--write` or `--read`, and tell the agent. Init keeps the agent's password. The `personMessage` has the command. See [Something else](/guides/refused/#something-else) and [CLI](/reference/cli/#basalt-init).

<a id="not-found"></a>

## `NOT_FOUND`

**What it means.** The run, change, approval or revert named in the call is not this agent's, or does not exist.

**Why it happens.** An agent sees only its own runs, changes, approvals and reverts. An id that exists for another agent reads exactly like an unknown id.

**Reasons.**

| Reason       | What happened                                             | What the agent does                                                                 |
| ------------ | --------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| `RUN`        | No such run for this agent.                               | Call `list_runs` and use an id from there.                                          |
| `CHANGE`     | No such change for this agent.                            | Call `list_runs`.                                                                   |
| `APPROVAL`   | No such approval for this agent.                          | Call `list_pending`.                                                                |
| `REVERT`     | No such revert for this agent.                            | Ask you for the revert id that `basalt revert` printed.                             |
| `TASK`       | The agent never ran in that task.                         | Call `list_runs`. You get a `personMessage` with the command to revert it yourself. |
| `LINK`       | A page link without its secret, or one that was replaced. | Pages only. You ask the agent for a new link.                                       |
| `LINK_RESET` | A revert link the agent reset to its own work.            | Pages only. You ask the agent for the new link.                                     |

**What you should do.** Usually nothing. For `TASK`, to undo work in a task your agent never ran in, run `basalt revert --agent NAME --task TASK_ID --open` with an agent that did, or ask that agent to request it. An agent never undoes another agent's work. An own scope with nothing to undo is [`NOTHING_TO_REVERT`](#nothing-to-revert), not this code.

<a id="sql-error"></a>

## `SQL_ERROR`

**What it means.** Postgres raised an error for the agent's statement.

**Why it happens.** A syntax error, an unknown column, a constraint violation or any other error that no more specific code covers. `sqlState` says which.

**Reasons.** `NO_ROWS_NOW`: the statement found no rows to act on, often because the row changed or was removed since the agent looked.

**What the agent should do.** Read `message` and `sqlState`, fix the statement and send it again. For `NO_ROWS_NOW`, read the row with `query` first.

**What you should do.** Nothing, unless the same error repeats. Then a constraint in your schema is probably refusing what the agent tries to do.

<a id="invalid-argument"></a>

## `INVALID_ARGUMENT`

**What it means.** The call has an argument that fails validation, or an argument the tool does not know. Unknown arguments are refused, never ignored.

**Why it happens.** The call does not match the tool's input schema. See [MCP tools](/reference/mcp-tools/).

**Reasons.**

| Reason             | What happened                                                             | What the agent does                                                                        |
| ------------------ | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| none               | The call fails validation.                                                | Fix the call and send it again.                                                            |
| `STALE_CURSOR`     | The rows of a preview changed between pages.                              | Send the same call again without `cursor`.                                                 |
| `SCOPE_FILTER`     | A `runs` or `tables` filter names something outside the agent's own work. | Drop the refused entries. Use `run_history` or `list_runs` to see its own runs and tables. |
| `SCOPE_FILTER_CAP` | More than 20 runs or tables in a filter.                                  | Use at most 20 entries, or no filter. You can narrow on the page.                          |

**What you should do.** Nothing.

<a id="unavailable"></a>

## `UNAVAILABLE`

**What it means.** The engine, the guard or your database cannot be reached, or the agent's setup has not run. Nothing was changed.

**Why it happens.** See the reasons. The `status` tool and `basalt status --agent NAME` name the cause and the fix.

**Reasons.**

| Reason                 | What happened                                   | What the agent does                                     | What you do                                                                                              |
| ---------------------- | ----------------------------------------------- | ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| `DATABASE_UNREACHABLE` | Your Postgres cannot be reached.                | Try the same call again after 10 seconds, then ask you. | Check the database is up.                                                                                |
| `CREDENTIAL_REJECTED`  | The database refused the agent's password.      | Stop.                                                   | Restart the engine. If that does not fix it, `basalt init --agent NAME --rotate-password`, then restart. |
| `GUARD_MISSING`        | The guard is not in this database.              | Stop.                                                   | Run `basalt init` again with the flags you used the first time.                                          |
| `GUARD_OUTDATED`       | The guard is older than this `basalt` needs.    | Stop. Only `status` works until you upgrade.            | `basalt upgrade --agent NAME`. See [Upgrade](/guides/upgrade/).                                          |
| `ENGINE_UNREACHABLE`   | The engine is not running.                      | Stop. `status` still works.                             | Start the engine. `basalt status --agent NAME` prints the command for your machine.                      |
| `PAGES_OFF`            | The engine does not serve the approval pages.   | Call `list_pending` and hand you the `approveCommand`.  | Run the command.                                                                                         |
| `SETUP_PENDING`        | The agent's setup from your laptop has not run. | End the turn. After you tell it, call `status`.         | Run `basalt init --setup-code CODE` on your own computer.                                                |
| `SETUP_EXPIRED`        | The setup code expired after 24 hours.          | End the turn.                                           | Ask the agent for a new code. See [A bot on its own computer](/guides/bot-own-computer/).                |

**What you should do.** See the table and [Something else](/guides/refused/#something-else). On an engine your bot runs, the commands that need your admin connection string run on your own computer, never on the bot's machine. The agent gives you a setup code instead.

<a id="internal"></a>

## `INTERNAL`

**What it means.** A defect in the engine or the guard. The action did not complete.

**Why it happens.** Something broke that should not (SQLSTATE `BS006` or `BS008`, or an engine fault). It is not caused by your data or the agent's call.

**What the agent should do.** Stop. Do not retry in a loop. Give you the `errorId`.

**What you should do.** Send the `errorId` to the project. See [Security](/security/).

## Warnings

A `query` or `write` that worked can carry `warnings`, and so can `run_start`. A warning has a `code`, a `message`, the `table` when it applies, and its own `personMessage` and `nextSteps`.

| Warning                   | What it means                                                                                                      | What to do                                                                                                                                       |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `RLS_HIDES_ROWS`          | Row-level security hides every row of a table in the statement from the agent role, and it read or changed 0 rows. | The agent asks you. To let it see the rows, run `basalt init` again with `--agent-rls=all-rows`. See [Hosts](/guides/hosts/#row-level-security). |
| `GUARD_UPGRADE_AVAILABLE` | The guard has non-breaking migrations to install. The work goes on.                                                | Run `basalt upgrade --agent NAME` when you are ready.                                                                                            |
