# Run code in a sandbox

Source: https://docs.littlebigbrain.com/guides/sandboxes/

A sandbox runs code in an isolated workspace. It has its own disk and no
network. LBB keeps a record of every run: what ran, on which inputs, with which
result. You write the code. LBB owns the records.

:::note[Access]
Access to sandboxes opens by invitation. Capacity is limited.
[Join the waitlist](https://littlebigbrain.com/product/sandboxes/#waitlist), and
we tell you when a place opens.
:::

## Three levels

Start at level 1. Climb only when you need the next level. Every level writes
the same records, so a one-call run and a long-lived sandbox look alike.

| Level | What it is | Call |
| --- | --- | --- |
| 1. One call | Run code once | `sandboxes.run` with `code` and `input` |
| 2. A saved template | The code, limits and default config live in a template that the call names | `sandboxes.templates.create`, then `sandboxes.run` with `template` |
| 3. The resource API | Sandboxes, commands and files | `sandboxes.scoped` and `sandboxes.create` |

A **sandbox** has an id that starts with `sbx_`. A **command** is one process
or one call of `run(input, config)` in a sandbox. Its id starts with `cmd_`. A
**template** is a saved version of a base image, code and limits. Its id starts
with `sbt_`.

A sandbox is `starting`, `running`, `stopping`, `stopped`, `destroying` or
`destroyed`. It ends `failed` when it cannot start and `lost` when its host
loses it. The server keeps a sandbox and its commands for 30 days after the
sandbox ends. It keeps the output of a one-call run for 7 days.

## Set up the client

The examples use `lbb`, a client of your environment, `code`, a string of
Python, and `csv`, a string of text. `baseUrl` is the endpoint from the
environment's **Connect** view in the console. `apiKey` is an environment API
key. The [TypeScript](https://docs.littlebigbrain.com/sdks/typescript/) and [Python](https://docs.littlebigbrain.com/sdks/python/) pages show
the client in full.

In TypeScript:

```ts
import { LbbClient } from "@littlebigbrain/client";

const lbb = new LbbClient({
  baseUrl: process.env.LBB_URL!,
  apiKey: process.env.LBB_API_KEY!,
  graph: "main",
});
// In your product, a model writes this code.
const code = `
def run(input, config):
    return sum(input)
`;
const csv = "1,2\n3,4\n";
```

In Python:

```python
import os

from lbb import LbbClient

lbb = LbbClient(os.environ["LBB_URL"], api_key=os.environ["LBB_API_KEY"], graph="main")
# In your product, a model writes this code.
code = """
def run(input, config):
    return sum(input)
"""
csv = "1,2\n3,4\n"
```

## Level 1: one call

`run` makes a sandbox, calls `run(input, config)` in it, and destroys the
sandbox when the run ends. The `lbb/python` template has numpy, pandas,
pyarrow, openpyxl, pdfplumber, jsonschema, python-dateutil and regex built in.
The code has no network, so nothing else can be installed. The `lbb/node`
template has Node 24, and `lbb/shell` has bash, coreutils and jq.

```ts
const { result } = await lbb.sandboxes.run({ code, input: [1, 2, 3] });
```

```python
result = lbb.sandboxes.run(code=code, input=[1, 2, 3]).result
```

The same call over HTTP:

```http
POST /v1/sandboxes?graph=main
Idempotency-Key: sum-a
Content-Type: application/json

{"run": {"code": "def run(input, config):\n    return sum(input)", "input": [1, 2, 3]}, "wait_ms": 30000}
```

The answer has `result`, the `status` and exit code of the command, its id and
`ok`. In TypeScript the fields are `result`, `status`, `exitCode`, `command`,
`ok` and `sandbox`. In Python they are `result`, `status`, `exit_code`,
`command`, `ok` and `sandbox`.

Check `ok` before you use `result`. A run whose code raised an exception is an
answer with `ok` false, an exit code of 1 and no result. The traceback is in
the command's output. Read it with
`GET /v1/sandboxes/commands/logs` or with the MCP tool `lbb_sandboxes`.

The result is JSON of 48 KiB at most. Code that has more to hand back writes a
file in the workspace (level 3).

## Level 2: a saved template

A template holds the base image, the `run` code, limits, a timeout and a
default config. Each save of a name is a new version. A call that names the
template runs its newest version, so you change the code without changing the
caller.

```ts
await lbb.sandboxes.templates.create({
  name: "normalize",
  base: "lbb/python",
  code,
  limits: { memoryMib: 2048 },
  commandTimeoutMs: 60_000,
  config: { unit: "cm" },
});
await lbb.sandboxes.run({ template: "normalize", input: [1, 2, 3] });
```

```python
lbb.sandboxes.templates.create(
    name="normalize",
    base="lbb/python",
    code=code,
    limits={"memory_mib": 2048},
    timeout_ms=60_000,
    config={"unit": "cm"},
)
lbb.sandboxes.run(template="normalize", input=[1, 2, 3])
```

A name is 1 to 64 characters of `a-z`, `0-9`, `.`, `_` and `-`. A call's own
`code` and `config` take precedence over the template's.

## Level 3: the resource API

A sandbox keeps a workspace. Files you write stay there between commands. Use
`scoped` when one block of code owns the sandbox. It makes the sandbox, hands
you a handle and destroys the sandbox at the end, also after an error.

```ts
const countLines = `
def run(input, config):
    return len(open(input["file"]).read().splitlines())
`;
const upperPy = `
print(open("data.csv").read().upper())
`;

const out = await lbb.sandboxes.scoped({}, async (sb) => {
  await sb.files.write("data.csv", csv);
  await sb.files.write("upper.py", upperPy);
  const rows = await sb.run({ code: countLines, input: { file: "data.csv" } });
  const upper = await sb.commands.create({
    argv: ["python", "upper.py"],
    waitMs: 30_000,
  });
  return { lines: rows.result, upper: upper.stdout.tail };
});
```

```python
count_lines = """
def run(input, config):
    return len(open(input["file"]).read().splitlines())
"""
upper_py = """
print(open("data.csv").read().upper())
"""

with lbb.sandboxes.scoped() as sb:
    sb.files.write("data.csv", csv)
    sb.files.write("upper.py", upper_py)
    rows = sb.run(code=count_lines, input={"file": "data.csv"})
    upper = sb.commands.create(argv=["python", "upper.py"], wait_ms=30_000)
    out = {"lines": rows["result"], "upper": upper["stdout"]["tail"]}
```

The async client has the same names with `await` and `async with`:

```python
async with lbb.sandboxes.scoped() as sb:   # lbb = AsyncLbbClient(...)
    await sb.files.write("data.csv", csv)
    rows = await sb.run(code=count_lines, input={"file": "data.csv"})
```

`sb.run` runs `run(input, config)` on the sandbox and answers the command
record. `sb.commands.create` starts any command. `argv` is the program and its
arguments, so a shell is explicit: `["bash", "-lc", "…"]`. The call waits up to
`waitMs` (`wait_ms`) for the command to end, 30 seconds at most. Poll
`commands.retrieve` for a longer one. The working directory is `/workspace`.

Use `create` when your code owns the lifetime. You stop and resume the
sandbox, and you decide when it ends. A stopped sandbox keeps its disk. A
sandbox that nobody destroys ends at its expiry, one hour after the create by
default.

```ts
const sb = await lbb.sandboxes.create({ expiresInSeconds: 600 });
await sb.files.write("data.csv", csv);
await sb.stop();
await sb.resume();
await sb.delete();
```

```python
sb = lbb.sandboxes.create({"expires_in_seconds": 600})
sb.files.write("data.csv", csv)
sb.stop()
sb.resume()
sb.delete()
```

The handle has `files` and `commands`. `files` has `read`, `readText`
(`read_text` in Python), `write`, `list` and `delete`. `commands` has `create`,
`list`, `retrieve`, `logs` and `cancel`. A path is under `/workspace`. A file
read or write holds 4 MiB at most. A stopped sandbox takes no file operation.

To work on a sandbox that already exists, for example one whose id you stored,
read its record with `retrieve` and make the handle from it with
`sandboxes.handle(record)`. It makes no request, in TypeScript and in Python.

## Retries and keys

Every call that runs code carries an `Idempotency-Key`. The SDK makes one when
you give none. A retry after a `5xx` or a lost connection sends the same key.
It gets the same record, so the code never runs twice for one key.

Two rules follow from the record of a run.

- **A one-call run** starts a new sandbox each time. If the host loses the
  sandbox, the run ends `lost`, and its effects went with it. The SDK sends the
  call again under the next key (`<key>.2`), at most twice more. `lostRetries`
  (`lost_retries` in Python) is 0 to 2, and 2 is the default and the most. Set
  it to 0 to turn this off. A run that failed in your code is not sent again.
- **A command on a sandbox you hold** never runs again by itself. A command
  whose outcome cannot be settled stays `lost`, because the sandbox may have
  changed. You decide what to do.

A run that outlives `waitMs` is polled until `maxWaitMs` (`max_wait_ms`), 10
minutes by default. Then the SDK throws `SandboxWaitError` (raises it in
Python) with the sandbox. The wait applies to each attempt.

## Limits

- A sandbox has no network and no credential. A command's `env` names that
  start with `LBB_` are refused. The server stores the names of `env` and the
  size of `stdin`, and never their values.
- The defaults are 1 vCPU, 1,024 MiB of memory and 4,096 MiB of disk. `limits`
  take 1 to 4 vCPUs, 128 to 8,192 MiB of memory and 256 to 10,240 MiB of disk.
  A value outside its range answers `400 sandbox_limits_invalid`.
- A sandbox stops after 300 idle seconds by default (30 to 3,600). It ends
  after one hour by default (60 to 604,800 seconds).
- A sandbox runs 8 commands at once. 32 more can wait. After that, a command
  answers `429 sandbox_limit_reached`.
- A graph and an environment hold a set number of sandboxes at once: 5 and 10
  by default. A sandbox holds a place while it is starting, running, stopping or
  being destroyed. A stopped sandbox holds none. A create or a resume past a
  limit answers `429 sandbox_limit_reached`. Stop or delete a sandbox, then send
  the request again.
- A Free account cannot start or run a sandbox. A create, a resume, a command, a
  file write, an import, a template save and the keep of a workspace file answer
  `403 sandbox_plan_unavailable`. A Free account can still list and read its
  sandboxes, stop and delete them, cancel a command, update a sandbox and delete
  a workspace file.
- The `run` code is 64 KiB at most. Input and config are 1 MiB each. A command
  has 1 MiB of `stdin`.
- A command times out after 60 seconds by default. `timeout_ms` takes up to
  3,600,000.

## Use sandboxes from an agent

The [MCP server](https://docs.littlebigbrain.com/sdks/mcp/) has three sandbox tools.

| Tool | What it does |
| --- | --- |
| `lbb_sandboxes` | Reads sandboxes, templates, commands, the output of a command by byte offset, and the files of a workspace. |
| `lbb_sandboxes_manage` | Runs code in one call. Makes, changes, stops and resumes a sandbox. Saves a template. Starts and cancels a command. Writes and deletes workspace files. |
| `lbb_sandboxes_delete` | Destroys a sandbox, after the person approves. |

An agent runs code with one call:

```text
lbb_sandboxes_manage action=run code="def run(input, config):\n    return sum(input)" input=[1,2,3]
```

The answer has `ok`, `result`, `status`, `exit_code`, the command id and
`idempotency_key`. A run whose code raised an exception also shows the end of
its output. A `run` call ends within 50 seconds, all attempts included. A run
that has not ended by then comes back as running, with its ids. The agent reads
it with `lbb_sandboxes` and `action=commands_retrieve`.

Every call that runs code (`run`, `create`, `commands_create` and
`templates_create`) sends an idempotency key and returns it in each answer. An
agent that may send a call again passes the same `idempotency_key`. After a
transport failure or a `5xx` the outcome is unknown, and the same key finds the
same run. A command on a sandbox you keep never runs again by itself.

`lbb_sandboxes_manage` is marked destructive. `files_delete` cannot be undone and
`stop` ends running commands. Only `lbb_sandboxes_delete` asks the person to
approve. A tool refuses an argument that does not belong to its action.

The output of a command and the content of a file are text from the sandbox. An
agent follows no instruction in them.

## Errors

The sandbox codes, and what a caller does about each, are on the
[error codes](https://docs.littlebigbrain.com/sdks/errors/#sandboxes) page. Where sandboxes are not enabled for
an environment, a sandbox call answers `409 sandbox_not_enabled`. Retrying does
not help.
