# Moving from Runloop

> Move Runloop devboxes to Prized sandboxes: the Prized command for each Runloop call, a short ported script, and what is not on Prized yet.

URL: https://prized.dev/docs/moving-from-runloop

Runloop shuts down on November 13, 2026 ([Runloop's notice](https://docs.runloop.ai/docs/transitions/overview)).

## 1. Export an API key [#1-export-an-api-key]

A Runloop devbox becomes a Prized [sandbox](https://prized.dev/docs/sandboxes): a microVM that boots in seconds, pauses with its processes running, and forks into live copies. Make a key with **Sandboxes: Write** on the [API keys page](https://prized.dev/dashboard/workspace/api-keys), then export it:

```bash
export PRIZED_API_KEY=dcp_…
```

## 2. Swap each call [#2-swap-each-call]

| Runloop                                          | Prized                                                               |
| ------------------------------------------------ | -------------------------------------------------------------------- |
| `runloop.devbox.create()`                        | `prized sandbox create --vcpu 2 --memory 4 --max-runtime 1h`         |
| `devbox.cmd.exec(command)`                       | `prized sandbox exec ID -- CMD`                                      |
| `devbox.file.write()`, `file.read()`             | `prized sandbox cp ./app.py ID:app.py`, and the reverse              |
| `devbox.suspend()`, `devbox.resume()`            | `prized sandbox pause ID --wait`, `prized sandbox resume ID`         |
| `devbox.snapshot_disk()`, then `create_devbox()` | `prized sandbox fork ID --count 3`, live copies of a running sandbox |
| `devbox.shutdown()`                              | `prized sandbox rm ID`                                               |

Each command has an HTTP route that takes the same key ([Sandbox API](https://prized.dev/docs/sandboxes-api)).

## 3. Port a script [#3-port-a-script]

The same steps in Python with `requests`:

```python title="prized_job.py"
import os, time, requests

API = "https://api.prized.dev/api/v1"
s = requests.Session()
s.headers["Authorization"] = f"Bearer {os.environ['PRIZED_API_KEY']}"

def run(box, command):
    return s.post(f"{box['toolboxBase']}/process/execute", json={"command": command}, timeout=600)

def ready(sandbox_id):  # running, and its first command answers
    while (box := s.get(f"{API}/sandboxes/{sandbox_id}").json()["sandbox"])["state"] == "starting":
        time.sleep(1)
    for _ in range(10):
        if run(box, "true").ok:
            return box
        time.sleep(1)

box = ready(s.post(f"{API}/sandboxes", json={"template": "prized-base-v1", "resources": {"vcpu": 2, "memoryMb": 4096}, "maxRuntimeMin": 60}).json()["sandbox"]["id"])
ids = [box["id"]]
try:
    print(run(box, "python3 --version").json()["stdout"])
    s.post(f"{API}/sandboxes/{box['id']}/pause", params={"wait": "paused"})
    s.post(f"{API}/sandboxes/{box['id']}/resume", params={"wait": "running"})
    ids += [copy["id"] for copy in s.post(f"{API}/sandboxes/{box['id']}/fork", json={"count": 2}).json()["sandboxes"]]
finally:
    for sandbox_id in reversed(ids):  # copies first: a copy still starting fails without its parent
        s.delete(f"{API}/sandboxes/{sandbox_id}")
```

A failed command still answers `200`, so read `exitCode`. A fork answers `409 fork_capacity` when the parent's machine is full; start a new sandbox and fork that ([Fork](https://prized.dev/docs/sandboxes#fork)).

## Not on Prized yet [#not-on-prized-yet]

* **Custom images and root.** Sandboxes start from two fixed templates without sudo, so Dockerfile blueprints, `apt`, and Docker need a [box](https://prized.dev/docs/boxes).
* **Long lives and saved snapshots.** A sandbox lives 24 hours at most, and a fork copies a running sandbox, not a saved snapshot.
* **Tunnels, SSH, named shells, network policies, Agent Gateways, and Axons.**

## Getting help moving [#getting-help-moving]

Email the support address shown on https://prized.dev/docs/troubleshooting with what you run on Runloop, and we will help you map it.
