> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.astropods.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.astropods.com/_mcp/server.

# Run code in a sandbox

A sandbox is a Linux machine your agent can run commands on and write files to, kept apart from everything else. Give your agent one when it needs to try code it wrote: run a script, install a package, test a fix.

By the end of this guide, your agent writes a Python script, runs it in a sandbox, and answers with the result.

> **Warning**
>
> Sandboxes are experimental. The API may change without a deprecation period.

## Before you start

* An agent project you can run with `ast project start`. [Your first project](/get-started) sets one up.
* The agent uses the TypeScript SDK. The sandbox client is not in the Python SDK yet.
* You are signed in with [`ast login`](/cli/top-level#login).

## Give your agent a sandbox

#### Say what the sandbox needs

A new sandbox has core utilities and nothing else, so list the languages and packages your agent's code uses. Add a `sandbox` section to `astropods.yml`:

**`astropods.yml`**

```yaml title="astropods.yml"
sandbox:
  toolchain: auto
  python:
    version: "3.12"
    packages: [numpy]
```

This sandbox gets Python 3.12 and `numpy`. `toolchain: auto` adds compilers when the sandbox installs packages, so a package that builds from source can. The [`sandbox` section](/astropods-package-spec#9-sandbox) of the spec lists everything else you can declare, such as system packages and setup commands.

#### Connect the sandbox to your agent

Give each conversation its own sandbox, so one user's files never show up in another's conversation.

#### Mastra

Give the agent a workspace backed by a sandbox. Mastra's built-in workspace tools then run commands and edit files there.

**`agent.ts`**

```typescript title="agent.ts"
import { Agent } from '@mastra/core/agent';
import { Workspace } from '@mastra/core/workspace';
import { AstroSandbox, serve } from '@astropods/adapter-mastra';

const agent = new Agent({
  id: 'coding-agent',
  name: 'Coding Agent',
  instructions: 'You write Python to answer questions. Run your code before you answer.',
  model: 'anthropic/claude-sonnet-4-6',
  // One sandbox per conversation.
  workspace: ({ requestContext }) =>
    new Workspace({
      sandbox: new AstroSandbox({ name: requestContext.get('threadId') }),
    }),
});

serve(agent);
```

#### Any framework

Wrap the sandbox in a tool your agent can call. `exec` runs a command and waits for it to finish.

**`run-python.ts`**

```typescript title="run-python.ts"
import { SandboxClient } from '@astropods/adapter-core';

const sandboxes = new SandboxClient();

// conversationId is the id of the conversation this call belongs to.
export async function runPython(conversationId: string, code: string) {
  await sandboxes.writeFile(conversationId, '/workspace/main.py', code);
  const result = await sandboxes.exec(conversationId, {
    command: ['python3', 'main.py'],
  });
  return result.exitCode === 0 ? result.stdout : `Failed:\n${result.stderr}`;
}
```

Register `runPython` as a tool the way your framework does it.

#### Try it locally

Start the agent:

```bash
ast project start
```

Open the chat at `http://localhost:3100` and ask:

> Use numpy to find the mean and standard deviation of 3, 8, 1, 9, 4. Run it.

The first message can take a minute while the sandbox installs Python and `numpy`. The agent then writes a script, runs it, and answers with the numbers. Later messages in the same conversation are quick.

Your local agent gets a real sandbox, the same kind a deployed agent gets. `ast project stop` deletes it.

#### Deploy

Push and deploy the agent as usual:

```bash
ast blueprint push my-agent
ast blueprint deploy my-agent
```

A deployed agent's sandboxes use the `sandbox` section it was deployed with. After you change the section, push and deploy again.

## More things your agent can do

Each recipe below is a task you can hand your agent, with what to declare and the calls a tool makes. A Mastra agent does all of these through its workspace tools, so it needs only the declaration. With any other framework, put the calls in a tool. Every snippet uses the `sandboxes` client and `conversationId` from the step above.

### Analyze a spreadsheet the user uploaded

Copy the upload into the sandbox, then let Python read it.

**`astropods.yml`**

```yaml title="astropods.yml"
sandbox:
  toolchain: auto
  python:
    packages: [pandas]
```

```typescript
import { readFile } from 'node:fs/promises';

await sandboxes.writeFileBytes(conversationId, '/workspace/sales.csv', await readFile(uploadedPath));
const { stdout } = await sandboxes.exec(conversationId, {
  command: ['python3', '-c', 'import pandas as pd; print(pd.read_csv("sales.csv").describe())'],
});
```

`uploadedPath` is where the user's file landed on your agent's disk. See [Files in chat](/messaging-sdk/files-in-chat).

### Read a PDF

Declare a system package, and its commands are ready to run.

**`astropods.yml`**

```yaml title="astropods.yml"
sandbox:
  toolchain: never
  packages: [poppler-utils]
```

```typescript
await sandboxes.writeFileBytes(conversationId, '/workspace/report.pdf', await readFile(uploadedPath));
const { stdout: text } = await sandboxes.exec(conversationId, {
  command: ['pdftotext', '-layout', 'report.pdf', '-'],
});
```

Packages come from the Amazon Linux 2023 repository. `sqlite`, `ImageMagick` and `graphviz` are there too. `ffmpeg` and `pandoc` are not.

### Make a chart

Have the agent write a plotting script, run it, and read the image back.

**`astropods.yml`**

```yaml title="astropods.yml"
sandbox:
  toolchain: auto
  python:
    packages: [matplotlib]
```

```typescript
await sandboxes.writeFile(conversationId, '/workspace/chart.py', `
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
plt.bar(["Mon", "Tue", "Wed"], [12, 19, 7])
plt.savefig("chart.png")
`);
await sandboxes.exec(conversationId, { command: ['python3', 'chart.py'] });
const png = await sandboxes.readFileBytes(conversationId, '/workspace/chart.png');
```

`png` is the image's bytes. Send it to the user as a download, as [Files in chat](/messaging-sdk/files-in-chat) shows.

### Run TypeScript

Declare Node and any command-line tool from npm.

**`astropods.yml`**

```yaml title="astropods.yml"
sandbox:
  toolchain: never
  node:
    version: "22"
    packages: [tsx]
```

```typescript
await sandboxes.writeFile(conversationId, '/workspace/hello.ts', 'const n: number = 6 * 7;\nconsole.log(n);\n');
const { stdout } = await sandboxes.exec(conversationId, { command: ['tsx', 'hello.ts'] }); // "42\n"
```

### Clone a repository and run its tests

`git` is always installed. An install and a test suite can outlast a single `exec`, so use `run`, which streams output and has no time limit.

**`astropods.yml`**

```yaml title="astropods.yml"
sandbox:
  toolchain: auto
  node:
    version: "22"
```

```typescript
await sandboxes.exec(conversationId, {
  command: ['git', 'clone', '--depth', '1', 'https://github.com/sindresorhus/slugify', 'repo'],
});
const result = await sandboxes.run(
  conversationId,
  { command: ['/bin/sh', '-c', 'npm install && npm test'], cwd: '/workspace/repo' },
  { onOutput: (chunk) => process.stdout.write(chunk.stdout) },
);
```

### Start a long job and check on it later

`spawn` starts a command and returns at once, so the job keeps going after the turn ends. On a later turn, `poll` reports whether it finished and what it printed.

```typescript
const job = await sandboxes.spawn(conversationId, { command: ['python3', 'train.py'] });
// Save job.processId with the conversation, then on a later turn:
const status = await sandboxes.poll(conversationId, job.processId);
return status.state === 'exited'
  ? `Done, exit code ${status.exitCode}`
  : `Still running:\n${status.stdout.slice(-500)}`;
```

### Pick up where the last message left off

Files stay in the sandbox for the whole conversation, so the agent can build on earlier work. A script it wrote three messages ago is still in `/workspace`, along with any data it downloaded. Ask the agent to change the script and run it again, and it edits the file in place.

## What to expect

**The first message in a conversation is the slow one.** A new sandbox installs everything you declared before your agent's first command runs. Sandboxes for later conversations usually start faster, because the install is reused.

**Files last for the conversation, not forever.** A sandbox is paused after 15 minutes without use and picks up where it left off on the next message. After an hour paused, it is removed along with its files. The next message gets a fresh sandbox with your declared software installed. Save anything that must outlast the conversation somewhere else.

**A long conversation keeps its sandbox.** A busy sandbox is replaced after 8 hours, and the replacement has everything the old one had: files, declared software, and anything the agent installed along the way.

**Long jobs belong in the background.** A single command can run for about 30 seconds. For an install, a build, or a test suite, use [`run`](/adapters/node/sandboxes#run), which has no time limit. With Mastra, the workspace already tells the model to run long work in the background.

**A failed install shows up as an error.** If a package does not exist or will not build, your agent's first command fails with the package manager's message. Fix the `sandbox` section, then deploy again, or restart `ast project start` when testing locally.

## Troubleshooting

| What you see                                            | What to do                                                                                                                                                                                                          |
| ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `this agent declares no sandbox`                        | Add a `sandbox` section to `astropods.yml`, then push and deploy.                                                                                                                                                   |
| `the sandbox declaration failed to install at packages` | A name under `packages` is wrong, or the package is not in the sandbox's package repository. The same error ends in `python packages` or `node packages` for a PyPI or npm name. Check the name, then deploy again. |
| `SandboxPreparingError`                                 | A large declaration took longer than 15 minutes to install. Pass a larger `prepareTimeoutSeconds` to the client, or declare less.                                                                                   |
| An error with status 402                                | The deployment reached its sandbox limit. Delete sandboxes you no longer need.                                                                                                                                      |

## Next steps

* [Sandboxes](/adapters/node/sandboxes) covers every client method, the full lifecycle, and limits.
* [Mastra adapter](/adapters/mastra#sandboxes) covers the Mastra workspace in more detail.
* [Astropods Spec, section 9](/astropods-package-spec#9-sandbox) lists every field of the `sandbox` section.