> Fetch the index of every Tonbo docs page as Markdown at https://tonbo.dev/llms.txt

# Quickstart: Pi Durable

Last updated 29 October 2026

How to use [Pi Durable](https://github.com/earendil-works/pi/blob/v1.1.0/packages/durable/README.md) is in Pi’s own README, from its [Quick Start](https://github.com/earendil-works/pi/blob/v1.1.0/packages/durable/README.md#quick-start) to [Persist and Resume](https://github.com/earendil-works/pi/blob/v1.1.0/packages/durable/README.md#persist-and-resume); this page moves its storage to a Tonbo database, so another machine can resume the conversation.

Run it on your laptop, in a sandbox or on any machine with Node.js 22.19 or later and internet access, with nothing to allowlist, on Linux (x64 or arm64) or macOS on Apple silicon; on any other platform, such as an Intel Mac, build the package’s SQLite extension from source first (see [Requirements](https://tonbo.dev/docs/databases/connect.md#requirements)).

## Sign in

Sign in at [tonbo.dev](https://tonbo.dev/login) with GitHub or Google. A new account asks you to choose a username: it names your personal account, which holds your databases, streams and keys, and appears in the console’s addresses. You can change it once every 90 days.

## Create a database

Create a database named `harness-1`. It goes to the default region, `us-east-1`, unless you choose another (see [Regions and URLs](https://tonbo.dev/docs/databases/manage.md#regions)): pick the one nearest where your program runs. With the CLI, name it with `--region`, as in `tonbo database create harness-1 --region ap-southeast-1`.

**Console**

1. Open [Databases](https://tonbo.dev/) in the console.
2. Choose **Create database** and name it `harness-1`.

**CLI**

```shell
npm i -g @tonbo/cli
tonbo login
tonbo database create harness-1
```

`tonbo login` signs you in through a browser on the same machine. On a machine without one, such as a remote server, set `TONBO_API_KEY` to a key that acts as you and is not narrowed, created in the console under **API keys**, and the CLI uses it instead.

**API**

With a key that acts as you and is not narrowed, in `TONBO_API_KEY`: create one in the console under **API keys** without narrowing it. The next step’s key is narrowed, and a narrowed key never creates databases.

```shell
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":"harness-1"}'
```

It answers `201` once the database is active, or `202` while it is still being created.

The database is creating for a moment, then active. Node.js code opens it by name, and Python code by its URL, which `tonbo database show harness-1` and the console show (see [Regions and URLs](https://tonbo.dev/docs/databases/manage.md#regions)).

## Create an API key

Give the program its own key, narrowed to `harness-1`: it reads and writes only that database, and may name one that does not exist yet. A key that acts as you and is not narrowed reaches every database and stream you can in its account (see [What a key can reach](https://tonbo.dev/docs/platform/api-keys.md#key-scope)).

**Console**

1. In the [console](https://tonbo.dev/), choose **API keys** in the sidebar.
2. Choose **New API key** and narrow it to the database `harness-1`.
3. Copy the key. It starts with `tbo_` and is shown once.

**CLI**

```shell
tonbo api-key create harness-1-app --database harness-1
```

It prints the key once, as `TONBO_API_KEY=...`, ready for the program’s environment. Put it before the command that runs the program, as the next step does, rather than exporting it in your shell: the CLI reads `TONBO_API_KEY` too, and would then act with the narrowed key.

On this machine, after `tonbo login`, a Node.js program can also open its databases with no key at all: `open()` falls back to your login (see [Connect](https://tonbo.dev/docs/databases/connect.md)). Python does not, and the narrowed key is what any program uses anywhere else.

## Install the packages

```shell
npm install --save-exact @tonbo/sqlite-ursula@0.11.2 \
  @earendil-works/pi-durable@1.1.0 @earendil-works/pi-ai@1.1.0 \
  @earendil-works/chord@1.1.0
```

Pi Durable’s own API is experimental and changes without notice between its releases, so pin the Pi versions you test with. Tonbo’s Streams acceptance test runs `@tonbo/sqlite-ursula` 0.11.2 with the Pi packages at 1.1.0.

Check that npm added the prebuilt extension for your platform:

```shell
npm ls --all | grep @tonbo/sqlite-ursula-
```

It prints the extension’s package, such as `@tonbo/sqlite-ursula-darwin-arm64` on a Mac with Apple silicon. If it prints nothing, npm skipped that optional package: install again without `--omit=optional`, or, on a platform with no prebuilt extension, build it from source (see [Requirements](https://tonbo.dev/docs/databases/connect.md#requirements)).

## Swap the storage

Pi’s [Persist and Resume](https://github.com/earendil-works/pi/blob/v1.1.0/packages/durable/README.md#persist-and-resume) opens the Harness on a SQLite file. On Tonbo, its import and its storage change, and nothing else:

**Persist and Resume, on Tonbo**

```text
- import { openNodeSqliteStorage } from "@earendil-works/pi-durable/storage/sqlite/node";
+ import { openUrsulaPiStorage } from "@tonbo/sqlite-ursula/tonbo/pi";

- const harness = await Harness.open(await openNodeSqliteStorage("./session.sqlite"), { models, registry }, context);
+ const harness = await Harness.open(await openUrsulaPiStorage("harness-1"), { models, registry }, context);
  const root = await harness.root(context); // the same root as last time
  harness.resume(); // continue any run the last process left unfinished
```

`openUrsulaPiStorage` opens the database by name with the key in `TONBO_API_KEY`, and `harness.close()` closes it with the Harness.

## Run a turn

A whole program, saved as `agent.mjs`. It reads the answer as Pi’s Quick Start does, from the entry the submission settled with:

**agent.mjs**

```js
import { BACKGROUND_CONTEXT as ctx } from "@earendil-works/chord/context";
import { contentText } from "@earendil-works/pi-ai";
import { createModels } from "@earendil-works/pi-ai/models";
import { openaiProvider } from "@earendil-works/pi-ai/providers/openai";
import { AssistantEntry, createRegistry, Harness } from "@earendil-works/pi-durable";
import { openUrsulaPiStorage } from "@tonbo/sqlite-ursula/tonbo/pi";

// node agent.mjs <request id> [stop]
const [requestId, mode] = process.argv.slice(2);
const models = createModels();
models.setProvider(openaiProvider()); // reads OPENAI_API_KEY

// Reads TONBO_API_KEY. CACHE_DIR plays another machine, with its own local copy.
const storage = await openUrsulaPiStorage("harness-1", { dir: process.env.CACHE_DIR });
const harness = await Harness.open(storage, { models, registry: createRegistry() }, ctx);
const root = await harness.root(ctx, { agent: { model: { provider: "openai", modelId: "gpt-6-sol" } } });

// The same request id finds the same submission instead of asking twice.
const input = { type: "input", requestId, content: "What is the capital of France?" };
const submission = await root.submit(input, ctx);
console.log("submission", submission.id);
if (mode === "stop") process.exit(); // stored; stop before the answer, as a crash would

const settled = await submission.wait(ctx);
if (settled.status === "done") {
  const answer = await root.commit((tx) => tx.entry(AssistantEntry, settled.answer), ctx);
  console.log(contentText(answer.model[0].content, ""));
} else console.log(settled.status, settled.reason);
await harness.close(ctx); // closes the storage too
```

In every shell you use below, export the key from the API key step and your OpenAI key:

```shell
export TONBO_API_KEY=tbo_... OPENAI_API_KEY=...
```

Run it with the request id `q1`. It prints the submission’s id and, once the model answered, the answer:

**Shell 1**

```text
$ node agent.mjs q1
submission 9
The capital of France is Paris.
```

The id and the model’s words differ for you; the shape of the output does not. `agent.mjs` never calls `harness.resume()`: submitting and waiting start Pi’s scheduler, which continues what the last process left unfinished. A process that only opens the Harness and waits calls `harness.resume()` itself (see [Persist and Resume](https://github.com/earendil-works/pi/blob/v1.1.0/packages/durable/README.md#persist-and-resume)).

## Finish the run from another machine

Stop a run midway and let a second process finish it. In shell 1, ask under a new request id with `stop`: the program exits as soon as the question is stored, before the model answers, as a crash would.

**Shell 1**

```text
$ node agent.mjs q2 stop
submission 17
```

In shell 2, run the same request with its own `CACHE_DIR`, which stands in for another machine: it has no local copy, so the database rebuilds from Tonbo. On another machine with the keys and `agent.mjs`, leave `CACHE_DIR` out.

**Shell 2**

```text
$ CACHE_DIR=./machine-b node agent.mjs q2
submission 17
The capital of France is Paris.
```

Shell 2 printed the same submission: the request id `q2` found the question shell 1 stored, so it was asked once, and `wait()` ran it to its answer.

## Open it from two shells at once

One process writes a database at a time: the last one to open it (see [When two instances run](https://tonbo.dev/docs/databases/how-it-works.md#takeover)). Save this beside `agent.mjs` as `hold.mjs`: it opens the database, waits for Enter, then writes to it.

**hold.mjs**

```js
import { createInterface } from "node:readline/promises";
import { BACKGROUND_CONTEXT as ctx } from "@earendil-works/chord/context";
import { createModels } from "@earendil-works/pi-ai/models";
import { createRegistry, Harness } from "@earendil-works/pi-durable";
import { openUrsulaPiStorage, UrsulaResetError } from "@tonbo/sqlite-ursula/tonbo/pi";

const storage = await openUrsulaPiStorage("harness-1");
const harness = await Harness.open(storage, { models: createModels(), registry: createRegistry() }, ctx);
const root = await harness.root(ctx);

const prompt = createInterface({ input: process.stdin, output: process.stdout });
await prompt.question("open; press Enter to write ");
prompt.close();
try {
  await root.submit({ type: "input", content: "And of Italy?" }, ctx);
  console.log("written");
  await harness.close(ctx);
} catch (error) {
  // The call that met the reset rejects with it; later calls wrap it as their cause.
  const reset = error instanceof UrsulaResetError ? error : error?.cause;
  if (!(reset instanceof UrsulaResetError)) throw error;
  console.log(reset.name, reset.kind, reset.action);
  process.exit(1);
}
```

In shell 1, start it and leave it waiting:

**Shell 1**

```text
$ node hold.mjs
open; press Enter to write
```

While it waits, run `agent.mjs` in shell 2, again with its own `CACHE_DIR`: one local copy serves one process at a time.

**Shell 2**

```text
$ CACHE_DIR=./machine-b node agent.mjs q3
submission 25
The capital of France is Paris.
```

Then press Enter in shell 1. Its write is refused:

**Shell 1**

```text
open; press Enter to write
UrsulaResetError fenced stop
```

Shell 2’s open made it the writer, so shell 1’s next commit failed with `UrsulaResetError` of kind `fenced` and action `stop`. Shell 1 stops rather than open the database again: opening it would take the database back from shell 2 (see [one writer](https://tonbo.dev/docs/databases/rules/one-writer.md)).

## When the storage is reset

On Pi 1.1, which this installs, a long outage rejects a Harness call with Pi’s `StorageRejected` and the session continues; from Pi 1.2 the call waits instead (see [backlog_full](https://tonbo.dev/docs/reference/nodejs/pi.md#backlog-full)). A reset rejects it with `UrsulaResetError`. On `reopen`, close and open again. On `stop`, another process took the database over: stop and alert rather than take it back.

**Closing and reopening**

```js
import { UrsulaResetError } from "@tonbo/sqlite-ursula";

async function open() {
  const storage = await openUrsulaPiStorage("harness-1");
  const harness = await Harness.open(storage, { models, registry }, context);
  harness.resume();
  return harness;
}

let harness = await open();

// Call this when a Harness call rejects.
async function onError(error) {
  if (!(error instanceof UrsulaResetError)) throw error;
  await harness.close(context);
  if (error.action === "stop") {
    // Another process owns the database. Opening it again would fence that one.
    console.error("database taken over by another writer, stopping", error);
    process.exit(1);
  }
  // "fix": the cause, such as a deleted database, must be fixed before an open.
  if (error.action !== "reopen") throw error;
  harness = await open();
}
```

Run one instance per database. On Kubernetes, use the `Recreate` strategy or a StatefulSet: a rolling update starts the new pod before the old one stops.

## Add it to a project

In a project of your own, with the program’s API key in `TONBO_API_KEY` where the project keeps its secrets, never in its code:

1. Create the database with `tonbo database create harness-1`, signed in with `tonbo login`, or in the console. `tonbo database show harness-1` prints `State:` `active` once it is ready.
2. Install, keeping the exact versions:

   ```shell
   npm install --save-exact @tonbo/sqlite-ursula@0.11.2 \
     @earendil-works/pi-durable@1.1.0 @earendil-works/pi-ai@1.1.0 \
     @earendil-works/chord@1.1.0
   ```

   `npm ls --all | grep @tonbo/sqlite-ursula-` then prints the prebuilt extension for your platform.
3. Where the Harness opens on `new MemoryStorage()`, open it on `await openUrsulaPiStorage("harness-1")` from `@tonbo/sqlite-ursula/tonbo/pi` instead, and call `harness.resume()` after `Harness.open` to continue any run the last process left unfinished.

Run one instance per database. One process writes a database at a time: the last one to open it (see [When two instances run](https://tonbo.dev/docs/databases/how-it-works.md#takeover)).

## Next steps

- [Connect from Node.js](https://tonbo.dev/docs/databases/connect.md): resolveDatabase, the key and its errors, synced(), and what the host needs.
- [Rules for databases](https://tonbo.dev/docs/databases/rules.md): One instance per database, stopping when a reset says stop, and the other rules.
- [Guarantees](https://tonbo.dev/docs/databases/how-it-works.md): Why one process writes a database at a time, and what happens to the first when a second opens it.
- [Cold starts](https://tonbo.dev/docs/databases/cold-starts.md): How long a new host takes to rebuild a conversation, and how to keep restarts from rebuilding.
- [Limits](https://tonbo.dev/docs/databases/limits.md): Transaction, snapshot and database size, requests per second, and databases per account.
- [Pricing](https://tonbo.dev/docs/databases/pricing.md): Writes, retention, the monthly allowance, and what happens when the balance runs out.
