# run()

The core primitive — one policy, one robot, one scene, and back comes a rate with n, a Wilson interval, and named failure clusters.

`roborama.run()` puts a policy on a physical robot in a versioned scene and
returns a statistic you can defend: a success rate with its sample size and
Wilson 95% interval, failure clusters, both meters, and the full episode
record. Every other primitive is a composition of this call. Everything else
on this page is a field on it.

## The call

*Example: roborama.run()*

**Python**

```python
import roborama  # reads ROBORAMA_API_KEY from the environment

run = roborama.run(
    robot="g1-edu-pro@fw2.3",            # embodiment @ pinned firmware
    environment="kitchen-std@v1.2",      # versioned catalogue scene
    policy=roborama.Policy.container(    # see policy packaging
        image="ghcr.io/acme/skill:v4",
        action_space="joint_delta_50hz",
        observation_contract="droid-3cam",
    ),
    task="load_dishwasher@v2",
    episodes="auto(ci=0.95, moe=0.03)",  # size n for ±3% at 95% — or an int
    perturbation={                       # the schedule IS the product spec
        "layout_jitter_mm": 25,
        "lighting": ["3000K", "5600K"],
        "distractors": "set-B",
        "seed": 42,
    },
    data={"retention": "30d", "train_on_failures": False},  # IP posture
    max_budget_usd=4_000,                # hard stop, metered live
)

print(run.result())
#  n=612  success_rate=0.874  ci95=(0.846, 0.898)
#  failure_clusters: [grasp_slip: 41, perception_miss: 22, collision: 9]
#  robot_hours=20.4  environment_hours=20.4  cost_usd=3_812
#  artifacts: mcap[], video[], ground_truth[], report_pdf
```

**TypeScript**

```typescript
import Roborama from "@roborama/sdk"; // reads ROBORAMA_API_KEY

const roborama = new Roborama();

const run = await roborama.runs.create({
  robot: "g1-edu-pro@fw2.3", // embodiment @ pinned firmware
  environment: "kitchen-std@v1.2", // versioned catalogue scene
  policy: {
    type: "container",
    image: "ghcr.io/acme/skill:v4",
    action_space: "joint_delta_50hz",
    observation_contract: "droid-3cam",
  },
  task: "load_dishwasher@v2",
  episodes: "auto(ci=0.95, moe=0.03)", // size n for ±3% at 95%
  perturbation: {
    layout_jitter_mm: 25,
    lighting: ["3000K", "5600K"],
    distractors: "set-B",
    seed: 42,
  },
  data: { retention: "30d", train_on_failures: false },
  max_budget_usd: 4000, // hard stop, metered live
});

const result = await run.result();
console.log(result);
// { n: 612, success_rate: 0.874, ci95: [0.846, 0.898], cost_usd: 3812 }
```

**cURL**

```bash
curl https://api.roborama.com/v1/runs \
  -H "Authorization: Bearer $ROBORAMA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "kind": "run",
    "robot": "g1-edu-pro@fw2.3",
    "environment": "kitchen-std@v1.2",
    "policy": {
      "type": "container",
      "image": "ghcr.io/acme/skill:v4",
      "action_space": "joint_delta_50hz",
      "observation_contract": "droid-3cam"
    },
    "task": "load_dishwasher@v2",
    "episodes": "auto(ci=0.95, moe=0.03)",
    "perturbation": {
      "layout_jitter_mm": 25,
      "lighting": ["3000K", "5600K"],
      "distractors": "set-B",
      "seed": 42
    },
    "data": { "retention": "30d", "train_on_failures": false },
    "max_budget_usd": 4000
  }'
```

**Agent (tool-use payload)**

```json
{
  "type": "tool_use",
  "name": "roborama_run",
  "input": {
    "robot": "g1-edu-pro@fw2.3",
    "environment": "kitchen-std@v1.2",
    "policy": {
      "type": "container",
      "image": "ghcr.io/acme/skill:v4",
      "action_space": "joint_delta_50hz",
      "observation_contract": "droid-3cam"
    },
    "task": "load_dishwasher@v2",
    "episodes": "auto(ci=0.95, moe=0.03)",
    "perturbation": {
      "layout_jitter_mm": 25,
      "lighting": ["3000K", "5600K"],
      "distractors": "set-B",
      "seed": 42
    },
    "data": { "retention": "30d", "train_on_failures": false },
    "max_budget_usd": 4000
  }
}
```

```text
n=612  success_rate=0.874  ci95=(0.846, 0.898)
failure_clusters: [grasp_slip: 41, perception_miss: 22, collision: 9]
robot_hours=20.4  environment_hours=20.4  cost_usd=3,812
artifacts: mcap[], video[], ground_truth[], report_pdf
```

Every field of that result is load-bearing: the rate ships as 0.874 with n=612
and ci95=(0.846, 0.898), the failures arrive pre-clustered
(grasp_slip: 41, perception_miss: 22, collision: 9), and both meters are on
the receipt.

## Fields

| Field | Type | Notes |
| --- | --- | --- |
| `robot` | `str` | `model@firmware`; `@latest` is allowed but the resolved pin is recorded |
| `environment` | `str` | catalogue id + revision, from bare `cell-a` to `surgical-replica@v3` |
| `policy` | `Policy` | `container`, `checkpoint`, or `endpoint`; contracts validated before any motor moves |
| `task` | `str` | versioned task id, e.g. `load_dishwasher@v2` |
| `episodes` | `int \| "auto(...)"` | auto-sizing solves for margin of error — statistics as an input, not an afterthought |
| `perturbation` | `dict` | seeded, versioned, replayable; declared in the result |
| `data.retention` | `str` | artifact retention window, e.g. `"30d"`; early deletion via `data.purge` is receipted |
| `data.train_on_failures` | `bool` | customer IP boundary; contractual default `False` |
| `max_budget_usd` | `int` | hard stop, metered live against both meters |
| `priority` | `"burst" \| "standard" \| "soak"` | maps to the rate-card tiers |
| `interventions` | `"none" \| "on_stall" \| "scripted"` | declared before the run; counts and timestamps ship in results, and any intervened episode is flagged in the statistics |

## Auto-sizing episodes

Pass an int if you already know your n. `episodes="auto(ci=0.95, moe=0.03)"`
solves for it instead: episodes keep scheduling until the margin of error
tightens to ±3 points at 95% confidence. The canonical run above stopped at
n=612. How the sizing works, and when a tighter margin is worth the episodes,
is covered in [Runs & statistics](/docs/concepts/runs-and-statistics/).

## The perturbation schedule

The schedule is the product spec. `layout_jitter_mm: 25` is a claim about the
messiness of the kitchens your policy is supposed to survive; the lighting
list and distractor set are the rest of that claim. The schedule is seeded and
versioned, so the same schedule and seed reproduce the same distribution of
scenes — which is what makes two runs comparable and a result replayable. The
realized values, not just the requested ones, are recorded per episode in the
`/meta` channel, alongside firmware, calibration age, and thermal state.

## Data posture

`data.train_on_failures` defaults to `False`, and the default is contractual:
your failure episodes are your IP, not our training set, unless you flip the
bit yourself. `data.retention` sets how long artifacts live;
`roborama.data.purge(run_id)` deletes early and returns a receipt. The full
posture — what comes back, who owns it, what we may touch — is specified in
the [data contract](/docs/concepts/data-contract/).

## Budget and priority

`max_budget_usd` is a hard stop, metered live against robot-hours and
environment-hours. A run that hits it halts with `budget_exceeded` and keeps
the partial result: n and the interval simply describe the episodes that ran.
The canonical run was capped at 4,000 and finished at 3,812.

> **A capped run is not a lost run:** `budget_exceeded` is a status, not a failure. You keep every episode,
> artifact, and statistic accrued up to the stop — often enough to decide
> whether the remaining margin is worth buying.

`priority` picks the queue: `"burst"` when you need the answer today,
`"standard"` for the default rate, `"soak"` for patient bulk iteration on
commodity cells.

## Interventions

Human help is a statistic-shaped hole unless it's declared.
`interventions` sets the policy before the run: `"none"` (the default —
a stalled episode is a failed episode), `"on_stall"` (an operator may
recover a stall), or `"scripted"` (declared recovery procedures only).
Whatever you declare, the counts and timestamps ship in the result, and any
intervened episode is flagged in the statistics — a rate propped up by
helping hands can't masquerade as autonomy. The canonical espresso run in
[/fixtures/runs.json](/fixtures/runs.json) declares `"none"` and reports
`count: 0`.

For tasks whose [Task Spec](/docs/concepts/tasks-and-method-transfer/)
declares stages or an instruction distribution, the result also carries
`stage_rates` and `instruction_breakdown` — each entry with its own n and
ci95.

## Where next

- [eval()](/docs/primitives/eval/) — the same discipline on a frozen, citable suite.
- [matrix()](/docs/primitives/matrix/) — this call, swept across embodiments × environments.
- [API reference](/docs/api-reference/) — the REST shape of `run()`, generated from [/openapi.json](/openapi.json).
