# Run your container

Submit any publicly pullable OCI image by digest — it runs GPU-adjacent to the cell, with no network in the control loop.

*Provider workflow: Docker, GitHub.*

Container is the packaging for anything custom: your runtime, your
preprocessing, your weights, one image. The cell runs it GPU-adjacent —
no network hop in the control loop, which is why this is the
lowest-latency packaging — and the run records the digest, so the
artifact is pinned as tightly as the robot's firmware.

## Prerequisites

### The image reference

The `image` field of `Policy.container()` is an ordinary OCI reference;
any registry works:

- Docker Hub — `docker.io/acme/skill@sha256:4e5b...`
- GHCR — `ghcr.io/acme/skill@sha256:4e5b...`

There are no pull-credential fields today: **the image must be pullable
without authentication** — public, or served from a registry mirror you
operate that allows anonymous pulls. Private-pull support does not
exist yet, and this page won't pretend otherwise.

Keep the two permission surfaces distinct. *Pull* permission is
registry-side and yours to configure — it governs who can fetch the
image, and anonymous pull does not have to mean listed or discoverable.
The *running* container's permissions are the cell's: it executes
GPU-adjacent with no outbound network in the control loop. That
isolation is the point of container mode — nothing between policy and
actuator — and it is the opposite trade from
[endpoint mode](/docs/guides/connect-your-endpoint/), which keeps
weights home by putting your network in the loop.

### Pin the digest

Tags move; digests don't. Resolve the tag you tested to its digest and
submit that:

```bash
docker buildx imagetools inspect ghcr.io/acme/skill:v4 \
  --format '{{println .Manifest.Digest}}'
```

```text
sha256:4e5b1a7c9d0f2a3b5c6d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a90
```

A digest identifies the complete artifact — code, weights,
preprocessing, runtime — which is more than a bare weights hash pins.
Two images with identical checkpoints and different preprocessing are
different policies, and their digests say so. If `v4` is re-pushed
tomorrow, a digest-pinned run still names exactly what ran.

### What the cell provides

Resources are provisioned GPU-adjacent per cell, sized to what the
image needs — CPU, GPU, memory. Your job is a server that boots when
the container starts: the cell runtime feeds it the observations named
by the declared `observation_contract` and expects actions in the
declared `action_space`, at its declared rate. Between episodes the
runtime signals an episode boundary, and your policy clears its
per-episode state — physical scene reset is the cell's job (scripted or
teleop-assisted), never your policy's. A formal wire-level spec —
routes and schemas — is not yet published; this page describes the
interface behaviorally because that is what exists to describe.

## Connect

*Example: run a digest-pinned image*

**Python**

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

DIGEST = ("sha256:4e5b1a7c9d0f2a3b5c6d8e9f0a1b2c3d"
          "4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a90")

policy = roborama.Policy.container(
    image="ghcr.io/acme/skill@" + DIGEST,
    action_space="joint_delta_50hz",
    observation_contract="droid-3cam",
)

run = roborama.run(
    robot="g1-edu-pro@fw2.3",
    environment="kitchen-std@v1.2",
    policy=policy,
    task="load_dishwasher@v2",
    episodes="auto(ci=0.95, moe=0.03)",
    max_budget_usd=4000,
)
print(run.id)
```

**TypeScript**

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

const roborama = new Roborama();

const DIGEST =
  "sha256:4e5b1a7c9d0f2a3b5c6d8e9f0a1b2c3d" +
  "4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a90";

const run = await roborama.runs.create({
  robot: "g1-edu-pro@fw2.3",
  environment: "kitchen-std@v1.2",
  policy: {
    type: "container",
    image: "ghcr.io/acme/skill@" + DIGEST,
    action_space: "joint_delta_50hz",
    observation_contract: "droid-3cam",
  },
  task: "load_dishwasher@v2",
  episodes: "auto(ci=0.95, moe=0.03)",
  max_budget_usd: 4000,
});
console.log(run.id);
```

**cURL**

```bash
# submit the image by digest — tags move, digests don't
DIGEST="sha256:4e5b1a7c9d0f2a3b5c6d8e9f0a1b2c3d"
DIGEST="${DIGEST}4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a90"

curl -sS https://api.roborama.com/v1/runs \
  -H "Authorization: Bearer $ROBORAMA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "robot": "g1-edu-pro@fw2.3",
    "environment": "kitchen-std@v1.2",
    "policy": {
      "type": "container",
      "image": "ghcr.io/acme/skill@'"$DIGEST"'",
      "action_space": "joint_delta_50hz",
      "observation_contract": "droid-3cam"
    },
    "task": "load_dishwasher@v2",
    "episodes": "auto(ci=0.95, moe=0.03)",
    "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)",
    "max_budget_usd": 4000
  }
}
```

The declared contracts do the same work as everywhere else: the
submission is validated against `g1-edu-pro@fw2.3` and the scene before
any motor moves, and a mismatch is a free rejection, not a burned
episode.

## Validate

Three layers, in the order they can catch you:

### Locally, in mock mode

`ROBORAMA_MOCK=1` runs the full loop against packaged fixtures — no API
key, no hardware, no charge. Wire it into the same CI job that builds
the image.

*Example: dry-run in mock mode*

**Python**

```python
import roborama

# Mock mode: no API key, no hardware. The whole loop runs
# offline against packaged fixtures — or set ROBORAMA_MOCK=1.
client = roborama.Client(mock=True)

quote = client.quote(
    robot="aloha2-pro",
    environment="cell-a",
    episodes=300,
)
print(quote)
# {robot_hours: 10.0, env_hours: 10.0, usd: 860, queue_eta: "2h"}

run = client.run(
    robot="aloha2-pro@fw1.1",
    environment="cell-a@v1.0",
    policy=roborama.Policy.checkpoint(
        hf="acme/act-so101-pick-v2",
        runtime="lerobot",
    ),
    task="pick_place@v1",
    episodes=300,
    max_budget_usd=1000,
)
print(run.result())
# n=300  rate=0.843  ci95=(0.797, 0.881)
```

**TypeScript**

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

// Mock mode: no API key, no hardware. The whole loop runs
// offline against packaged fixtures — or set ROBORAMA_MOCK=1.
const roborama = new Roborama({ mock: true });

const quote = await roborama.quote({
  robot: "aloha2-pro",
  environment: "cell-a",
  episodes: 300,
});
console.log(quote);
// { robot_hours: 10.0, env_hours: 10.0, usd: 860, queue_eta: "2h" }

const run = await roborama.runs.create({
  robot: "aloha2-pro@fw1.1",
  environment: "cell-a@v1.0",
  policy: {
    type: "checkpoint",
    hf: "acme/act-so101-pick-v2",
    runtime: "lerobot",
  },
  task: "pick_place@v1",
  episodes: 300,
  max_budget_usd: 1000,
});
console.log(await run.result());
// n=300  rate=0.843  ci95=(0.797, 0.881)
```

**cURL**

```bash
# Mock mode is an SDK feature — curl always talks to the live
# API. ROBORAMA_MOCK=1 flips any SDK entry point to packaged
# fixtures: no API key, no hardware, no spend.
ROBORAMA_MOCK=1 python - <<'PY'
import roborama

quote = roborama.quote(
    robot="aloha2-pro", environment="cell-a", episodes=300)
print(quote)

run = roborama.run(
    robot="aloha2-pro@fw1.1",
    environment="cell-a@v1.0",
    policy=roborama.Policy.checkpoint(
        hf="acme/act-so101-pick-v2", runtime="lerobot"),
    task="pick_place@v1",
    episodes=300,
    max_budget_usd=1000,
)
print(run.result())
PY
```

**Agent (tool-use payload)**

```json
{
  "type": "tool_use",
  "name": "roborama_quote",
  "input": {
    "robot": "aloha2-pro",
    "environment": "cell-a",
    "episodes": 300
  }
}
```

### At admission — free

The declared `action_space` and `observation_contract` are validated
server-side at submission. A mismatch returns
`contract_validation_failed` (422); a rejected submission costs
nothing.

### At cell startup

The cell pulls the image and boots your server. A pull failure — a
typo'd reference, a deleted tag, a registry demanding auth — surfaces
*here*, not at admission, and there is no dedicated error code for it
today: expect a run that fails at startup rather than a clean
rejection. Digest-pin and keep the image anonymously pullable to make
this layer boring.

## Quote

A quote prices both meters before you commit. The fixture job — 600
episodes of `g1-edu-pro` in `kitchen-std` — resolves to 20.0
robot-hours × $148 plus 20.0 environment-hours × $39: $3,740, queue ETA
about 6 hours. Expiry and priorities are covered in
[billing & budgets](/docs/guides/billing-and-budgets/).

*Example: quote before you run*

**Python**

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

quote = roborama.quote(
    robot="g1-edu-pro",
    environment="kitchen-std",
    episodes=600,
)
print(quote)
# {robot_hours: 20.0, env_hours: 20.0, usd: 3740, queue_eta: "6h"}
```

**TypeScript**

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

const roborama = new Roborama();

const quote = await roborama.quote({
  robot: "g1-edu-pro",
  environment: "kitchen-std",
  episodes: 600,
});
console.log(quote);
// { robot_hours: 20.0, env_hours: 20.0, usd: 3740, queue_eta: "6h" }
```

**cURL**

```bash
curl https://api.roborama.com/v1/quote \
  -H "Authorization: Bearer $ROBORAMA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "robot": "g1-edu-pro",
    "environment": "kitchen-std",
    "episodes": 600
  }'
```

**Agent (tool-use payload)**

```json
{
  "type": "tool_use",
  "name": "roborama_quote",
  "input": {
    "robot": "g1-edu-pro",
    "environment": "kitchen-std",
    "episodes": 600
  }
}
```

## Evaluate

Submission is an ordinary [`run()`](/docs/primitives/run/); the fields
not set here — perturbation schedule, retention, priority — are that
page's. Follow it live while episodes tick:

*Example: watch it live*

**Python**

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

run = roborama.runs.get("run_8842")

for event in run.watch():   # live: episode ticker + WebRTC stream URLs
    print(event.episode, event.status)

stream = roborama.streams.get("cell-g1-04")
print(stream)
# {webrtc: "wss://streams.roborama.com/cells/cell-g1-04/webrtc",
#  mjpeg: "https://streams.roborama.com/cells/cell-g1-04/mjpeg",
#  viewer_token: "vt_7Kq2mHentXw4"}
```

**TypeScript**

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

const roborama = new Roborama();

const run = await roborama.runs.get("run_8842");

for await (const event of run.watch()) {
  // live: episode ticker + WebRTC stream URLs
  console.log(event.episode, event.status);
}

const stream = await roborama.streams.get("cell-g1-04");
console.log(stream);
// { webrtc: "wss://streams.roborama.com/cells/cell-g1-04/webrtc",
//   mjpeg: "https://streams.roborama.com/cells/cell-g1-04/mjpeg",
//   viewer_token: "vt_7Kq2mHentXw4" }
```

**cURL**

```bash
# live event stream (server-sent events): episode ticker + status
curl -N https://api.roborama.com/v1/runs/run_8842/events \
  -H "Authorization: Bearer $ROBORAMA_API_KEY"

# cell video stream descriptor
curl https://api.roborama.com/v1/streams/cell-g1-04 \
  -H "Authorization: Bearer $ROBORAMA_API_KEY"
```

**Agent (tool-use payload)**

```json
[
  {
    "type": "tool_use",
    "name": "roborama_watch_run",
    "input": { "run_id": "run_8842" }
  },
  {
    "type": "tool_use",
    "name": "roborama_get_stream",
    "input": { "cell": "cell-g1-04" }
  }
]
```

## Inspect results

> **Fixture output:** Numbers here are the canonical container run `run_8842`:
> `load_dishwasher@v2` on `g1-edu-pro@fw2.3` in `kitchen-std@v1.2`.

```text
n=612  rate=0.874  ci95=(0.846, 0.898)  cost_usd=3812
```

The result also carries the failure clusters and both meters; every
episode ships MCAP telemetry, video, and a replay spec, and
`run.report(format="pdf")` renders the citable verification report.
Retention windows and export formats belong to the
[data contract](/docs/concepts/data-contract/).

## Compare versions

Same robot, same scene, same task, same declared contracts — different
digest. That single-variable diff is what makes
[`compare()`](/docs/primitives/compare/) meaningful: paired episodes,
same initial conditions for both images, roughly half the episodes to
separate them.

## Troubleshoot

| Code | Symptom | Fix |
| --- | --- | --- |
| `contract_validation_failed` | 422 at submission; nothing ran | the declared `action_space` or `observation_contract` doesn't match the pinned robot and scene — fix the declaration or the pin |
| `firmware_pin_unavailable` | 422 at submission | the requested `model@firmware` isn't installed on any cell — check `robots.list()` for available revisions |
| `budget_exceeded` | run halts mid-suite at the cap | partial results are kept with honest statistics; raise `max_budget_usd` and resubmit if the interval is too wide |
| `queue_timeout` | run never scheduled within its priority window | resubmit, or use `burst` priority |

## Where next

- [Package code as a container](/docs/guides/package-your-policy/) — the Dockerfile and CI workflow that produce the digest this page submits.
- [Policies](/docs/concepts/policies/) — the three packagings and the contracts they share.
- [Connect your endpoint](/docs/guides/connect-your-endpoint/) — the inverse trade: weights stay home, your network enters the loop.
- [Data contract](/docs/concepts/data-contract/) — what each episode records, and who owns it.
