When something is refused
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, which lists them all. To see the engine, the guard and the passkey in one place, run:
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 |
Refused |
| “Held for your approval” | APPROVAL_REQUIRED |
Held |
| The row changed since it was read | STALE_READ |
Stale read |
| More than 50 rows in one action | ROW_LIMIT_EXCEEDED |
Row limit |
| The write had no reason | INTENT_REQUIRED |
Intent missing |
| A read or an undo took too long | QUERY_TIMEOUT |
Timeout |
| No permission, no connection, nothing to undo | Others | Something else |
Refused
Section titled “Refused”{ "code": "STATEMENT_REFUSED", "reason": "DDL_NOT_ALLOWED" }
The code is 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_KEYandNOT_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 plainSELECT,INSERT,UPDATEorDELETEand send it to the right tool. Reads go toquery. Writes go towrite. NOT_REPEATABLE: the write contains a value that changes between transactions, such asnow(). 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.
Held for your approval: Delete 1 row in public.leads + 2 cascaded rows (public.lead_notes 2).
The code is 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:
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.
Related codes:
APPROVAL_EXPIRED: nobody approved within 24 hours. The agent asks you whether to send the write again.APPROVAL_INVALIDhas 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. Runbasalt passkey list --agent NAME, add a passkey withbasalt 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 checksrun_historyand stops.NOT_THIS_AGENT: the approval belongs to another agent. The agent callslist_pendingfor its own.DIFFERENT_WRITE: the write is not the one you approved. The agent sends the held write unchanged.
NO_PASSKEY(insideAPPROVAL_REQUIRED): no passkey is registered, so nothing can be approved. Runbasalt init --agent NAME --add-passkey. See Passkeys.
Stale read
Section titled “Stale read”STALE_READ: public.leads {"id": 1} read at xmin ..., now ...
The code is 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
Section titled “Row limit”{ "code": "ROW_LIMIT_EXCEEDED" }
The code is 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 afterwards.
Intent missing
Section titled “Intent missing”{ "code": "INTENT_REQUIRED" }
The code is 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.
Timeout
Section titled “Timeout”{ "code": "QUERY_TIMEOUT", "reason": "TIME_LIMIT" }
The code is 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:
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. 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 and Configuration.
Something else
Section titled “Something else”| Code | What happened | What to do |
|---|---|---|
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 |
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 |
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 |
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 |
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 |
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 |
An undo has nothing to restore. | Nothing. The rows were only read, or your team or your app changed all of them. |
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 |
A defect in the engine or the guard. | The agent stops. Send the error id to the project. See Security. |
UNAVAILABLE has a reason:
ENGINE_UNREACHABLE: the engine is not running. Start it.basalt status --agent NAMEprints 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, runbasalt init --agent NAMEagain with--rotate-password, then restart the engine.GUARD_MISSING: the guard is not in this database. Runbasalt initagain with the flags you used the first time.GUARD_OUTDATED: the guard is older than this version of basalt. Runbasalt upgrade --agent NAME. See 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.
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.
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.
- Errors: every code, every reason.
- For agents: how an agent should read these results.
- Rules: what is allowed, held and refused by default.
BasaltDB · Apache-2.0