How to run Runtime sandboxes from GitHub Actions
Make a key with runtime keys create, store it as the repository secret RUNTIME_API_KEY, and run npx withruntime in a workflow step.
On Runtime a CI job pays for the CPU its tests use, not the time a runner holds a VM. A 2 vCPU, 4 GiB sandbox costs $0.03125 an hour while it waits and $0.08 an hour with both CPUs busy, and a new one ran its first Python command in a median 351 ms on 24 September 2026 (speed). Each job gets a fresh Firecracker microVM with its own kernel, so a test that runs untrusted or agent-written code cannot reach the runner.
1. Make a key for CI
A runner has no browser, so it uses a key. Make one from your own terminal and pipe it straight into the repository's secrets with the GitHub CLI:
Terminalnpx withruntime keys create --name ci --daily-limit 25 | gh secret set RUNTIME_API_KEYThe command prints a link and a code. An owner, admin or developer of the
account opens the link, checks the code, sees the key's name, access and
limit, and chooses Create key. The key is then printed once, alone on
standard output, so it lands in gh secret set and nowhere else. Runtime
keeps only a hash of it.
--daily-limit 25 caps what this key may commit in any 24 hours at $25. A
job past the cap gets spending_limit_reached and is charged nothing.
2. Add the workflow
YAML# .github/workflows/sandbox-tests.ymlname: sandbox-testson: [push, pull_request]jobs: test: runs-on: ubuntu-latest timeout-minutes: 20 steps: - uses: actions/checkout@v6 - uses: actions/setup-node@v7 with: node-version: 24 - name: Run the suite in a sandbox env: RUNTIME_API_KEY: ${{ secrets.RUNTIME_API_KEY }} run: npx withruntime sandbox run --vcpu 2 -- python3 -c 'print(6 * 7)'sandbox run creates a sandbox, runs the command, prints its output, stops
the sandbox and exits with the command's own exit code, so a failing test
fails the step. The CLI needs Node 22 or later, which setup-node pins.
3. Ship your code into the sandbox
For a real suite, a short script copies the checkout in and runs it. Call it
from a step with npx tsx ci/sandbox.ts or python3 ci/sandbox.py, with the
same env block:
TypeScriptimport { Runtime } from "withruntime";// Queue for up to ten minutes when many jobs start at once.const runtime = new Runtime({ waitForCapacityMs: 600_000 });await using sbx = await runtime.sandboxes.create({ timeoutSeconds: 1200, onLeaseEnd: "stop" });await sbx.files.upload(".", "/workspace/repo");const run = await sbx.exec("cd repo && pip install -r requirements.txt && python3 -m pytest -q", { timeoutMs: 900_000, // the default is 60 seconds onStdout: (text) => process.stdout.write(text), onStderr: (text) => process.stderr.write(text),});process.exitCode = run.exitCode ?? 1;Pythonimport sysfrom withruntime import Runtimewith Runtime(wait_for_capacity=600) as runtime: with runtime.sandboxes.create(timeout_seconds=1200, on_lease_end="stop") as sbx: sbx.files.upload(".", "/workspace/repo") run = sbx.exec( "cd repo && pip install -r requirements.txt && python3 -m pytest -q", timeout_ms=900_000, # the default is 60 seconds ) print(run.stdout, run.stderr) sys.exit(0 if run.exit_code == 0 else 1)await using and with stop the sandbox when the block ends, even after an
exception, so a failed job leaves nothing running.
Settings that matter in CI
| Setting | What it does in a workflow |
|---|---|
RUNTIME_API_KEY |
Overrides any saved connection; the only credential a runner needs |
CI (set by GitHub) |
A missing key is an error instead of a browser approval |
--daily-limit on keys create |
The most the key may commit in 24 hours |
--read-only on keys create |
A key that reads everything and changes nothing, for status checks |
waitForCapacityMs / wait_for_capacity |
How long a create queues for room; two minutes by default |
timeoutSeconds, onLeaseEnd: "stop" |
A hard end for a sandbox a crashed job never stopped |
--no-internet, --allow <host> |
Network rules for the sandbox, set on the host |
A paid account runs 100 sandboxes at once to start; the free trial runs eight. A burst of matrix jobs past either limit waits in the SDK instead of failing.
Mistakes and how Runtime handles them
- Pull requests from forks. GitHub passes no secrets, except
GITHUB_TOKEN, to a workflow triggered from a forked repository. The step then has no key, and becauseCIis set, the CLI fails at once with a missing-key error rather than hanging on a login link. - Putting the key in the command line. It would sit in the log and the
process list. Pass it only through
env, as above; the SDKs and CLI readRUNTIME_API_KEYby themselves. - Losing the key. It is printed once. Revoke it at API keys and make another; no key can create, list or revoke keys.
- A suite that runs past a minute.
exectimes out after 60 seconds by default and returnstimedOut: truewith the output so far, so the scripts above raisetimeoutMsto 15 minutes; the most is 24 hours. - A retried job creating a second sandbox. Every SDK write carries an idempotency key, and timeouts, 429 and 503 are retried with it, so a retried create never makes two sandboxes.
- Someone leaves the team. Keys they made are revoked with their membership. Make the CI key as a long-standing member, or remake it.
Related
- The runtime CLI for every
keys createoption - How to call the REST API with curl for steps with no Node
- Agent evals and SWE-bench for large parallel runs
- How to turn off sandbox internet
Sources
Checked 25 September 2026.
- Using secrets in GitHub Actions
- Workflow syntax for GitHub Actions
- Events that trigger workflows
- actions/setup-node
Facts on this page were checked on 25 September 2026.