> ## Documentation Index
> Fetch the complete documentation index at: https://docs.pome.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Sandboxes

> What a sandbox is — one id, one lifetime, one to three digital twins — how long it lives, and the two different limits that both happen to be three.

A **sandbox** is what you start when you want your agent to work against Pome:
one id, one lifetime, and one to three digital twins your agent talks to. It is
also the unit your plan counts, so *"how many can I have open at once"* is a
question about sandboxes, never about twins.

Every other page in these docs uses the word. This one defines it.

## What one sandbox holds

|                                |                                                                                                                                                                                                                  |
| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **One id**                     | Shaped `ses_…`. It appears in every twin URL as `/s/<id>/…`. The wire keeps the older spelling — `session_id` in request bodies, `/v1/sessions` alongside `/v1/sandboxes` — and both spellings work permanently. |
| **One to three digital twins** | `github`, `stripe`, `slack`, `gmail`, `linear`. The first one you name is the primary twin: the default attribution for criteria that do not name one themselves.                                                |
| **One bearer token**           | The sandbox's `agent_token`. Short-lived, scoped to this sandbox, and the only credential the agent under test needs. It is **not** your `pme_` team API key — see [Authentication](/docs/api#authentication).   |
| **One lifetime**               | 30 minutes, with a 30-minute idle timer running alongside it.                                                                                                                                                    |

The twins inside a sandbox are not independent of it. They share its id, its
bearer token, its lifetime and its line on the bill. **What they do not share is
state.** Each keeps its own world and writes its own tape, so a write through
GitHub is not visible to Slack. Your agent is what carries a fact from one twin
to the other; the platform moves nothing between them.

That is what makes a cross-twin task worth setting rather than a limitation of
one. The [Cross-twin consistency](/docs/examples/cross-twin-consistency) example can assert
that a GitHub decision reached Slack precisely *because* nothing propagates it
— its committed defect is an agent that decides correctly in GitHub and never
tells Slack, and an exam whose platform carried the fact across could never
catch that. [Two twins, one sandbox](/docs/multi-twin-sandboxes) walks it
end to end with both tapes on screen.

State never crosses *between* sandboxes. Each one boots its twins from a declared
starting state, so two sandboxes on the same seed find the same world.

A sandbox is also all-or-nothing. If one of its twins cannot come up, the whole
sandbox fails to start rather than starting partly — you never get a half-built
world your agent then works in.

## How long a sandbox lives

Two clocks run, and whichever expires first ends the sandbox:

| Clock        | Length                      | Reset by                                                                                                                                        |
| ------------ | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| **Lifetime** | 30 minutes from create      | Nothing. It is a hard ceiling.                                                                                                                  |
| **Idle**     | 30 minutes without activity | Any call your agent makes through a twin, or an explicit [`POST /v1/sandboxes/:id/heartbeat`](/docs/api/endpoints#post-v1sandboxesidheartbeat). |

Past either one the sandbox is closed and every call against it answers `410`
`session_expired`. Nothing about it is recoverable; start a new one.

<Warning>
  **Finalize while the sandbox is still live.** Pome writes the run row *at*
  finalize, and finalize reads the tape off the twins — which are gone once the
  sandbox closes. A sandbox you let expire leaves nothing to score, and no run
  you can go back for.
</Warning>

Stopping a sandbox yourself has the same consequence and says so out loud:
[`DELETE /v1/sandboxes/:id`](/docs/api/endpoints#delete-v1sandboxesid) and
[`pome sandbox stop`](/docs/cli/sandbox#sandbox-stop) both **refuse** an
ungraded sandbox unless you confirm you meant to discard its run.

## The two limits that are both three

One number causes more confusion here than the rest of the platform put
together, because two unrelated limits can both read *three*.

| Limit                    | What it bounds                                                   | Where it comes from                                   | Past it                                 |
| ------------------------ | ---------------------------------------------------------------- | ----------------------------------------------------- | --------------------------------------- |
| **Twins per sandbox**    | How many digital twins live **inside one** sandbox.              | The platform. Fixed at three, the same on every plan. | `422`, `details.error: too_many_twins`. |
| **Concurrent sandboxes** | How many sandboxes your team may have **open at the same time**. | Your plan. Moves when the plan moves.                 | `402`, `type: quota_exceeded`.          |

They measure different things — one is *depth*, the other is *width* — and they
move independently. If your plan's concurrency allowance also happens to be
three, that is two numbers colliding, not one rule: raising your plan raises the
second and never the first, and no plan has ever raised the first.

Read your own concurrency number rather than assuming it.
[`GET /v1/usage`](/docs/api/endpoints#get-v1usage) reports `sessions_used`
against `sessions_quota`, and `sessions_used` counts sandboxes **open right
now** — not sandboxes started this month — so it falls again the moment you
finalize or stop one. Dashboard → **Usage** reads the same counter.

<Note>
  Because concurrency is a *ceiling* rather than a monthly budget, a `402` from
  sandbox create is not a wall you wait out. Finalize an open sandbox and the slot
  frees immediately. See [Billing semantics](/docs/agent-evals) for which `402` is
  which.
</Note>

## One twin, or three?

Both are true, of different doors. The platform allows three; the dashboard's
own selector currently offers one.

| Door                                                         | Twins per sandbox today                              |
| ------------------------------------------------------------ | ---------------------------------------------------- |
| [`POST /v1/sandboxes`](/docs/api/endpoints#post-v1sandboxes) | Up to three — `"twins": ["github", "slack"]`.        |
| [`pome sandbox create`](/docs/cli/sandbox)                   | Up to three — repeat `--twin`.                       |
| `run_task` over [MCP tools](/docs/mcp/tools)                 | Up to three, taken from the task's own `twins` list. |
| Dashboard → **Twins** → **Start sandbox**                    | **One.** The selector accepts a single twin.         |

So the dashboard's limit is a limit of that screen, not of the platform. To
start a multi-twin sandbox today, use the CLI, the REST API, or a task that
declares more than one twin.

## What a sandbox gives you that a local twin does not

[`pome twin start <twin>`](/docs/cli/twin) runs a digital twin on your own
machine with no account and no sandbox. You get the twin, and you get its
recorded tape from `GET /_pome/events` — enough to drive an agent and to score
it however you like. The [Get started](/run-a-twin) path is built entirely
on it.

What it does not give you is a **score**. There is no local scoring: the CLI
captures and the cloud grades. Everything below needs a sandbox, and there is no
offline equivalent of any of it —

* a **verdict** against a task's `[code]` criteria, with the evidence pointer behind it;
* a **run** that persists — its trace, its tape, its state diffs — instead of a
  tape that dies with the process;
* **trials**: `n` independent attempts at one task, which is how reliability gets
  measured rather than asserted;
* the **narrator's** summary and handoff;
* anything on the [dashboard](/docs/dashboard).

`pome run --local` sits on the same line: it captures a trace and grades
nothing. [`pome eval <run-dir>`](/docs/cli/eval) is what sends that trace up for
a verdict.

The choice is not "hosted or local for the same result". It is:

* **A twin and a tape you score yourself** → a local twin. No account.
* **Pome's verdict on your agent** → a sandbox.

## Starting one

| Surface       | How                                                                                                                                          |
| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| **CLI**       | [`pome sandbox create --twin github`](/docs/cli/sandbox) — prints the connection details.                                                    |
| **REST**      | [`POST /v1/sandboxes`](/docs/api/endpoints#post-v1sandboxes) — with your own [seed](/docs/api/endpoints#seeds), or the twin's default world. |
| **MCP**       | `run_task` provisions one for the task and hands back the per-twin URLs. See [MCP tools](/docs/mcp/tools).                                   |
| **Dashboard** | **Twins** → pick an available twin → **Start sandbox**. Loading the page never starts one on its own.                                        |

A run started by [`pome run`](/docs/cli/run) creates and tears down its own
sandbox; you do not manage one by hand for an ordinary run.

## What a sandbox costs

Starting a sandbox costs no agent eval. Provisioning twins, launching your
agent, watching it work and reading its tape are all free — the eval is charged
when the run is **graded at finalize**, and at no other moment.

What a sandbox does consume while it is open is a **concurrency slot**, which is
the plan limit above.

[Billing semantics](/docs/agent-evals) has the full rule: what counts as a
scored run, when you are not charged, and which `402` means which. For what a
plan costs and what each one includes, see
[pome.sh pricing](https://pome.sh/#pricing) — the prices live there, and only
there, so there is nothing here to drift out of date.

## Next

<CardGroup cols={2}>
  <Card title="Billing semantics" icon="receipt" href="/docs/agent-evals">
    What consumes a meter, what is free, and the three `402`s worth telling apart.
  </Card>

  <Card title="Dashboard" icon="gauge" href="/docs/dashboard">
    Where the sandboxes you start, and the runs they produce, show up.
  </Card>

  <Card title="pome sandbox" icon="terminal" href="/docs/cli/sandbox">
    Create, list, and stop sandboxes from a terminal.
  </Card>

  <Card title="REST API" icon="webhook" href="/docs/api">
    The control plane that starts them, and the shape every error takes.
  </Card>
</CardGroup>
