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

# Run Copies of an Agent

> Start N copies of one unattended manifest, watch the run, read what each wrote, and stop it.

**Before You Begin:** A BoxRouter API key, an [SDK](https://boxrouter.ai/docs/sdk.md) or the CLI, and a provider key or [Credits](https://boxrouter.ai/docs/credits.md). [Set Up Your Account](https://boxrouter.ai/docs/setup.md) and [install the CLI](https://boxrouter.ai/docs/cli.md#install) or an SDK.

A run starts N copies of one manifest at once. Every copy gets the same repository revision, setup,
files, secrets, and command. Use it for several attempts at one task whose results you compare yourself.

A finished copy's sandbox can keep billing past the end of its lifetime. Read the results,
then [stop the run](#stop-and-clean-up) to destroy its sandboxes.

If `--copies` returns `409 capability_unavailable` with outcome `not_started`, the run was not accepted
and no sandbox started. Run the agent without copies.

## What a Run Is

* A copy's command must finish without anyone typing, so the manifest sets `interactive: false`.
  A run refuses an interactive manifest.
* Copies are independent. A copy that fails to start doesn't stop the others, and BoxRouter doesn't
  retry it.
* Starting a run prints a run ID. You read and stop the run with it.
* Copies bill like any sandbox: your provider keys or your
  [Credits](https://boxrouter.ai/docs/credits.md) pay for them. With neither, the run is accepted and every copy ends `start_failed` with
  `insufficient_credit`. `--provider` pins where the copies run.

Each copy can tell itself apart with two variables:

| Variable | Value |
| - | - |
| `BOXROUTER_COPY_INDEX` | This copy's position, counting from zero. |
| `BOXROUTER_COPY_COUNT` | How many copies the run started. |

## Start a Run

```yaml title="boxrouter.yaml" theme={"theme":"css-variables"}
name: copies-demo
interactive: false
command: echo "copy $BOXROUTER_COPY_INDEX of $BOXROUTER_COPY_COUNT" > /tmp/result.txt
```

The CLI command needs `uuidgen`. Each example prints an idempotency key before sending the request. The SDK examples pass the manifest above as an object.

```python Python theme={"theme":"css-variables"}
import sys
from uuid import uuid4

from boxrouter import BoxRouter
from boxrouter.models import AgentManifest

manifest = AgentManifest(
    name="copies-demo",
    interactive=False,
    command='echo "copy $BOXROUTER_COPY_INDEX of $BOXROUTER_COPY_COUNT" > /tmp/result.txt',
)

with BoxRouter() as client:
    run_key = str(uuid4())
    print(f"Idempotency key: {run_key}", file=sys.stderr, flush=True)
    run = client.sandboxes.launch_run(manifest=manifest, copies=2, idempotency_key=run_key)
    print(f"Run {run.id} started: {run.copies} copies")
    for copy in run.items:
        print(f"Copy {copy.index}: {copy.status}")
```

```typescript TypeScript theme={"theme":"css-variables"}
import { randomUUID } from 'node:crypto';

import { BoxRouter } from '@boxrouter/sdk';

const client = new BoxRouter();
try {
  const runKey = randomUUID();
  console.error(`Idempotency key: ${runKey}`);
  const run = await client.api.launchRuns.create({
    body: {
      copies: 2,
      manifest: {
        name: 'copies-demo',
        interactive: false,
        command: 'echo "copy $BOXROUTER_COPY_INDEX of $BOXROUTER_COPY_COUNT" > /tmp/result.txt',
      },
    },
    headers: { 'Idempotency-Key': runKey },
  });
  console.log(`Run ${run.id} started: ${run.copies} copies`);
  for (const copy of run.items) console.log(`Copy ${copy.index}: ${copy.status}`);
} finally {
  await client.close();
}
```

```bash CLI theme={"theme":"css-variables"}
RUN_KEY=$(uuidgen) &&
printf 'Idempotency key: %s\n' "$RUN_KEY" >&2 &&
boxrouter launch --manifest boxrouter.yaml --copies 2 --idempotency-key "$RUN_KEY"
```

If the response is lost, repeat only the launch command with the same key and unchanged manifest.
The replay returns the same run instead of starting another. Keep the printed key if you close the shell.

The command returns when the run is accepted, not when the copies finish:

Output

```text theme={"theme":"css-variables"}
Run run_842d0b17-fa1f-4924-8552-86a7999cf0f7 started: 2 copies
Copy 0: queued
Copy 1: queued
```

One run starts at most 100 copies. A larger count is refused with `429 run_limit_exceeded` before
anything starts; a count below 1 or not a whole number is refused as `invalid_request`.

A run refuses these three things:

* `--repo` together with `--copies`. Put the repository in the manifest's `code` field instead.
* Interactive catalog agents.
* `secret_values`. Save the secrets first (see [secrets](https://boxrouter.ai/docs/launch/secrets.md)).

## Read the Run

```python Python theme={"theme":"css-variables"}
from boxrouter import BoxRouter
from boxrouter.models import LaunchRunDocument

with BoxRouter() as client:
    run = client.api.launch_runs.get("RUN_ID")
    if not isinstance(run, LaunchRunDocument):
        raise RuntimeError("The run was not returned")
    print(f"Run {run.id}: {run.copies} copies")
    for copy in run.items:
        print(f"Copy {copy.index}: {copy.status} sandbox={copy.sandbox_id} exit={copy.exit_code} finished={copy.finished_at}")
```

```typescript TypeScript theme={"theme":"css-variables"}
import { BoxRouter } from '@boxrouter/sdk';

const client = new BoxRouter();
try {
  const run = await client.api.launchRuns.get({ path: { run_id: 'RUN_ID' } });
  console.log(`Run ${run.id}: ${run.copies} copies`);
  for (const copy of run.items) {
    console.log(`Copy ${copy.index}: ${copy.status} sandbox=${copy.sandbox_id} exit=${copy.exit_code} finished=${copy.finished_at}`);
  }
} finally {
  await client.close();
}
```

```bash CLI theme={"theme":"css-variables"}
boxrouter launch status RUN_ID
```

Output

```text theme={"theme":"css-variables"}
Run run_842d0b17-fa1f-4924-8552-86a7999cf0f7: finished, 2 copies
Copy 0: finished sandbox=e33e2208-2801-432d-a213-c45a27a46648 exit=0 finished=2026-10-29T10:53:05.555321Z
Copy 1: finished sandbox=abc692ea-d44d-4c45-a01c-5a7d61c42d74 exit=0 finished=2026-10-29T10:53:10.558514Z
```

By default, this reads stored state without contacting a sandbox. Add `--results` in the CLI, or `include: 'command'` (`include=` in Python)
on the API read, to query the running sandboxes. The CLI also summarizes the copies as a run state; the SDK examples print each copy's own status.

A copy can be `queued`, `preparing`, `started`, `start_failed`, `stopping`, `stopped`, `finished` or
`unknown`. A failed copy includes its error `code`, message and `fix`.

The run's state describes the copies together:

* `running`: not every copy has ended yet.
* `finished`: every copy has ended and all started.
* `failed`: every copy has ended and at least one did not start.
* `stopped`: the run was stopped.

For each `started` copy, `--results` also asks its sandbox what the command is doing: `running`, `exited`
with the exit code, `timed_out` or `unknown`.

* It waits five seconds in total, so a slow or unreachable sandbox reads `unknown` instead of delaying the rest.
* It never returns output.

## Get Your Results

BoxRouter keeps each copy's exit code and finish time. Files the command wrote exist only on its
sandbox's disk. Read them before you stop the run.

```python Python theme={"theme":"css-variables"}
from boxrouter import BoxRouter

with BoxRouter() as client:
    data = client.sandboxes.get("SANDBOX_ID").files.read("/tmp/result.txt")
    print(data.decode(), end="")
```

```typescript TypeScript theme={"theme":"css-variables"}
import { BoxRouter } from '@boxrouter/sdk';

const client = new BoxRouter();
try {
  const data = await (await client.sandboxes.get('SANDBOX_ID')).files.read('/tmp/result.txt');
  process.stdout.write(new TextDecoder().decode(data));
} finally {
  await client.close();
}
```

```bash CLI theme={"theme":"css-variables"}
boxrouter read SANDBOX_ID --path /tmp/result.txt
```

Output

```text theme={"theme":"css-variables"}
copy 0 of 2
```

Take `SANDBOX_ID` from the copy's `sandbox=` field. Without `code`, the command uses the sandbox's
starting directory. With `code`, it runs in the checkout directory under the sandbox user's `$HOME`.
Set an absolute output path, as in the example, when you need to read a file later.

BoxRouter does not collect files for you; read results before stopping the run, or have the agent
push a branch.

`read` is limited by the route's `file_transfer_bytes` (see [limits](https://boxrouter.ai/docs/limits.md#limits-your-route-sets)).
For larger results, have the agent push a branch. To keep files across sandbox runs, attach a
[Volume](https://boxrouter.ai/docs/volumes.md) when the sandbox starts.

## Stop and Clean Up

To stop one copy, use its zero-based index from the run status:

```python Python theme={"theme":"css-variables"}
from boxrouter import BoxRouter

with BoxRouter() as client:
    client.api.launch_runs.destroy_copy("RUN_ID", 0)
```

```typescript TypeScript theme={"theme":"css-variables"}
import { BoxRouter } from '@boxrouter/sdk';

const client = new BoxRouter();
try {
  await client.api.launchRuns.destroyCopy({ path: { run_id: 'RUN_ID', copy_index: 0 } });
} finally {
  await client.close();
}
```

```bash CLI theme={"theme":"css-variables"}
boxrouter api launch-runs destroy-copy RUN_ID 0
```

This records a cleanup request for copy 0 without stopping the other copies, even if the copy has not
started yet. Read the run again until the copy is `stopped` or is `start_failed` with no sandbox ID.
`stopping` means cleanup is not yet confirmed.

To stop the run:

```python Python theme={"theme":"css-variables"}
from boxrouter import BoxRouter
from boxrouter.models import LaunchRunStopAccepted

with BoxRouter() as client:
    stopped = client.api.launch_runs.stop("RUN_ID")
    if not isinstance(stopped, LaunchRunStopAccepted):
        raise RuntimeError("The stop request was not accepted")
    print(f"Run {stopped.id}: stop requested at {stopped.stopped_at}")
```

```typescript TypeScript theme={"theme":"css-variables"}
import { BoxRouter } from '@boxrouter/sdk';

const client = new BoxRouter();
try {
  const stopped = await client.api.launchRuns.stop({ path: { run_id: 'RUN_ID' } });
  console.log(`Run ${stopped.id}: stop requested at ${stopped.stopped_at}`);
} finally {
  await client.close();
}
```

```bash CLI theme={"theme":"css-variables"}
boxrouter launch stop RUN_ID
```

Output

```text theme={"theme":"css-variables"}
Run run_842d0b17-fa1f-4924-8552-86a7999cf0f7: stop requested at 2026-10-29T10:55:09.205215Z
```

Stop records the request and returns. BoxRouter then ends copies still working and destroys every
sandbox the run owns, including those of finished copies, so read their files first.

Cleanup is asynchronous: read the run again until each copy is `stopped` or is `start_failed` with no
sandbox ID. A failed copy without a sandbox keeps its `start_failed` status. Other statuses mean cleanup
is still in progress or unconfirmed.

Copies keep the exit code and finish time they recorded. Stopping twice is safe, and a run ID from another
workspace reads as not found.

## Next Steps

> [card] [Configure a Launch Manifest](https://boxrouter.ai/docs/launch/manifest.md)
>
> Write the unattended manifest a run needs.

> [card] [Supply an Agent's Secrets](https://boxrouter.ai/docs/launch/secrets.md)
>
> Save the secrets each copy reads.

> [card] [Launch Your Own Agent](https://boxrouter.ai/docs/launch/your-own-agent.md)
>
> A repository, a manifest or an image instead of the catalog.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.