# Using Sandboxes

> **Info:** Sandboxes are available on Hatchet Cloud only.

> **Warning:** Sandboxes are a Labs feature and may change in future releases. In the
>   dashboard, open <strong>Settings</strong>, go to <strong>Labs Preview</strong>{" "}
>   and turn on <strong>Sandboxes</strong>.

Durable tasks can use the Hatchet Sandbox SDK to create and manage the lifecycle of sandboxes and carry out operations inside them.

> **Info:** The sandbox SDK is available for TypeScript. Python, Go and Ruby are coming
>   soon. Join our <a href="https://hatchet.run/discord">Discord</a> to stay up to
>   date.

#### Typescript

Install `@hatchet-dev/sandbox` next to the Hatchet SDK and import it once. It adds `hatchet.sandbox` to your Hatchet client.

```bash
pnpm add @hatchet-dev/sandbox @hatchet-dev/typescript-sdk
```

```typescript_sandbox
import { HatchetClient } from '@hatchet-dev/typescript-sdk/v1';
import '@hatchet-dev/sandbox';

export const hatchet = HatchetClient.init();
```

Inside a [durable task](/v1/durable-tasks), create a sandbox, run a command, and stop it.

```typescript_sandbox
export const session = hatchet.durableTask({
  name: 'agent-session',
  executionTimeout: '1h',
  fn: async (input: { conversationId: string }) => {
    const sandbox = await hatchet.sandbox.create({
      id: input.conversationId,
      provider: 'e2b',
      metadata: { conversation: input.conversationId },
    });

    await sandbox.files.write('main.py', 'print("hello from the sandbox")');
    const result = await sandbox.exec('python3 main.py');

    await sandbox.kill();
    return { stdout: result.stdout, exitCode: result.exitCode };
  },
});
```

`provider` is required when the tenant has more than one provider.

## Running commands

`exec` runs a shell command in the workspace and returns its output once it finishes.

#### Typescript

```typescript_sandbox
export async function runTests(sandboxId: string) {
  const sandbox = await hatchet.sandbox.connect(sandboxId);

  const result = await sandbox.exec('npm test', { timeoutMs: 5 * 60 * 1000 });

  if (result.exitCode === 124) {
    throw new Error(`tests timed out:\n${result.stderr}`);
  }

  return { passed: result.exitCode === 0, output: result.stdout };
}
```

- The result has `stdout`, `stderr` and `exitCode`. A non-zero exit code is a result, not an error.
- `timeoutMs` stops the command after that long. A timed-out command returns exit code `124`.

## Working with files

`sandbox.files` reads and writes the workspace. Paths are relative to it.

#### Typescript

```typescript_sandbox
export async function prepareProject(sandboxId: string, files: Record<string, string>) {
  const sandbox = await hatchet.sandbox.connect(sandboxId);

  await sandbox.files.write(
    Object.entries(files).map(([path, data]) => ({ path: `src/${path}`, data }))
  );
  await sandbox.files.write('config/app.json', JSON.stringify({ debug: true }));

  const entries = await sandbox.files.list('src');
  const config = await sandbox.files.read('config/app.json');
  const hasLock = await sandbox.files.exists('src/package-lock.json');

  await sandbox.files.remove('src/.DS_Store');

  return { files: entries.map((e) => e.name), config: JSON.parse(config), hasLock };
}
```

- `read`, `write`, `list`, `exists`, `getInfo`, `makeDir`, `rename`, `copy` and `remove` are the file operations. A path outside the workspace throws a `FilesystemError` with code `invalid_path`.
- `read` and `write` carry up to 2 MiB per file. Larger files are not supported on Hatchet Cloud providers yet. A larger `read` fails with code `too_large` and a larger `write` with code `invalid`. One workaround is to fetch the file inside the sandbox and operate on it there.

## Resuming a paused sandbox

Every sandbox has an ID. Pass your own with `create({ id })` or take `sandbox.sandboxId`. From anywhere with a Hatchet client, connect by that ID.

#### Typescript

```typescript_sandbox
export async function gitStatus(sandboxId: string) {
  const sandbox = await hatchet.sandbox.connect(sandboxId);
  const result = await sandbox.exec('git status --short');
  return result.stdout;
}
```

If the sandbox was stopped, because it was idle or you called `kill`, `connect` starts it again from its latest snapshot.

## Forks

Start a new sandbox from any snapshot with `create({ from })`. A snapshot ID comes from the `snapshotId` on a call's result, or from `latestSnapshot` in `hatchet.sandbox.list()` once the sandbox was killed. Create two from the same snapshot and you have a fork, which is how you try two approaches from the same starting point.

#### Typescript

```typescript_sandbox
export async function fork(repo: string) {
  const base = await hatchet.sandbox.create({ provider: 'e2b', metadata: { repo } });
  await base.exec(`git clone ${repo} . && npm ci`);
  await base.kill();

  const [info] = await hatchet.sandbox.list({ sandboxId: base.sandboxId });
  const from = info.latestSnapshot!;

  const [a, b] = await Promise.all([
    hatchet.sandbox.create({ from, inheritParentMetadata: true }),
    hatchet.sandbox.create({ from, inheritParentMetadata: true }),
  ]);

  return [a.sandboxId, b.sandboxId];
}
```

A fork always runs on the provider that holds its snapshot.

## Finding and cleaning up

`hatchet.sandbox.list({ metadata })` returns the sandboxes whose tags include every pair you pass. `hatchet.sandbox.delete({ metadata })` deletes every match with all of its snapshots, and returns their IDs. Both filters are subset matches, so `{ conversation: "42" }` matches a sandbox tagged `{ conversation: "42", user: "ada" }`.

#### Typescript

```typescript_sandbox
export async function cleanup(conversationId: string) {
  const sandboxes = await hatchet.sandbox.list({ metadata: { conversation: conversationId } });
  for (const info of sandboxes) {
    console.log(info.sandboxId, info.live ? 'running' : 'stopped');
  }

  const deleted = await hatchet.sandbox.delete({ metadata: { conversation: conversationId } });
  return deleted;
}
```

`kill()` stops the machine and keeps the snapshots, so `connect` still works and the provider stops billing you for a running machine. `delete()` removes the sandbox and its snapshots from the provider.

> **Info:** Idle sandboxes stop on their own after the provider's max idle time, 3600
>   seconds by default. Stopped sandboxes still hold their snapshots at the
>   provider until you delete them.
