One database per agent run

Last updated 29 October 2026

View .md

Use this when each run of an agent, in a sandbox that goes away afterwards, needs SQLite state of its own: an orchestrator creates the run’s database, the sandbox opens it with a key that reaches only runs, syncs after each step, and the orchestrator deletes the database once the run’s results are safe.

Who holds which key

  • The orchestrator, the program that starts runs, holds a key that is not narrowed, acting as you or as an Agent of the account: an identity of the account with its own keys and the role you give it, created on Members. Only such a key creates and deletes databases (see What a key can reach). The role member is enough: it creates and deletes databases and creates its own keys. An owner or admin creates the Agent and its first key, which only they may do.
  • Each sandbox holds a key narrowed to the runs’ databases: it reads and writes them, and never creates or deletes one.
  • Each run gets a database named run- and the run’s id, such as run-42. A name is unique in the account while its database exists (see Names), and the account holds up to 10,000 databases at once (see Limits).

Create the run’s database

Before it starts the sandbox, the orchestrator creates the database, with its key in TONBO_API_KEY:

curl -sS https://api.tonbo.dev/v1/databases \
  -H "Authorization: Bearer $TONBO_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"name":"run-42"}'

It answers 201 once the database is active, or 202 while it is still being created, and opening never creates a database. Keep one Idempotency-Key per run and send it again on a retry: the retry then answers the same database instead of creating a second one (see Create a database). After a 202, read the database until its state is active:

curl -sS https://api.tonbo.dev/v1/databases/run-42 \
  -H "Authorization: Bearer $TONBO_API_KEY"

Its answer holds the database under data, so wait in a loop such as this one, with jq:

until [ "$(curl -sS https://api.tonbo.dev/v1/databases/run-42 \
  -H "Authorization: Bearer $TONBO_API_KEY" | jq -r .data.state)" = active ]; do
  sleep 1
done

Give the sandboxes a key

One key narrowed to the prefix reaches every run’s database, those created later included, and nothing else:

tonbo api-key create run-sandboxes --database-prefix run-

Every sandbox that holds it can reach every run’s database. Where runs must not reach each other, give each run a key of its own, narrowed to its database, and revoke it after the run; a key may name a database before it exists:

tonbo api-key create run-42-sandbox --database run-42

The orchestrator, with the role member, creates these keys for itself, so they act as the orchestrator, narrowed. Hand the key to the sandbox through its environment, as TONBO_API_KEY, from your secret store.

Run the sandbox

The sandbox opens the run’s database by name, with its local copy in /tmp/tonbo-cache, a directory any sandbox can write, and syncs after each step:

run.mjs
import { open } from "@tonbo/sqlite-ursula/tonbo";

// The run's database, by name, with the sandbox's key in TONBO_API_KEY,
// and its local copy in a directory the sandbox can write.
const db = await open("run-42", { dir: "/tmp/tonbo-cache" });
db.exec("CREATE TABLE IF NOT EXISTS steps(id INTEGER PRIMARY KEY, body TEXT)");

// steps: the run's work, as async functions that return what to keep.
for (const step of steps) {
  const result = await step();
  db.prepare("INSERT INTO steps(body) VALUES (?)").run(result);
  await db.synced(); // from here, losing the machine does not lose this step
}
db.close();

A database works in any Node.js process with a writable local directory for its cache, /tmp included, as long as only one process opens it at a time. A process that starts without the last one’s directory, such as a fresh sandbox, a new container or a serverless function, rebuilds the local copy from Tonbo when it opens the database, a cold start, and its open counts one write. Killed with its disk, a process can lose every commit since its last resolved synced(), so await synced() after each step that must survive, not only before it exits. Processes that open the same database at once, as concurrent invocations of a function do, take it over from each other, and the commits of all but the last are refused.

So run one sandbox per run: a second one that opens the same database becomes its writer (see When two instances run).

Delete it when the run is done

A database lives until it is deleted or its expiry passes. Once the run’s results are safe, the orchestrator deletes it:

curl -sS -X DELETE https://api.tonbo.dev/v1/databases/run-42 \
  -H "Authorization: Bearer $TONBO_API_KEY"

Or with the CLI, where --yes confirms without a terminal to ask:

tonbo database delete run-42 --yes

To have Tonbo delete a run’s database even when the run’s cleanup never happens, create it with an expiry, such as a week out: expires_at in the creation’s body (see Expiry). Deletion cannot be undone: export anything you keep first (see Export a copy). It costs nothing, and until it completes the database still counts toward the account’s databases.

What it costs

Each run counts one write for its creation, one for the sandbox’s open, and one for each of its commits up to 10 KiB, and its storage is charged for the days it is kept. The worked example prices a thousand runs a day.