# JavaScript and TypeScript SDK `withruntime` is one client for every Runtime Cloud product. It has no dependencies, runs on Node 22 or later and Bun, and ships the `runtime` CLI. ```bash no-run npm install withruntime ``` The client finds its key by itself: `RUNTIME_API_KEY` when it is set, and otherwise the connection this machine saved when it was connected (any `npx withruntime` command connects it, with one browser approval). On a server, put a key from https://withruntime.com/account/keys in `RUNTIME_API_KEY` from your secret manager. Never put it in source code, a URL, a browser bundle or a command-line argument. With no key anywhere, the first call fails with `missing_api_key` and says how to get one. ## Hello, sandbox ```ts import { Sandbox } from "withruntime"; await using sbx = await Sandbox.create(); const result = await sbx.exec("python3 -c 'print(6 * 7)'"); console.log(result.exitCode, result.stdout); ``` `Sandbox.create()` takes no required arguments and returns once the sandbox is running. `await using` stops it when the block ends, even after an error. It needs Node 24, Bun, Deno or TypeScript; in plain JavaScript on Node 22, write `const sbx = ...` and call `await sbx.stop()` in a `finally` block. With no arguments you get the free trial while it lasts, the default region, and 2 vCPU, 4 GiB of memory and a 4 GiB disk for up to 30 minutes. Every field is optional: ```ts import { Runtime } from "withruntime"; const runtime = new Runtime(); // RUNTIME_API_KEY, or this machine's connection const sbx = await runtime.sandboxes.create({ name: "tests-42", labels: { team: "search", job: "42" }, vcpu: 2, memoryMiB: 4096, diskMiB: 8192, timeoutSeconds: 900, onLeaseEnd: "stop", network: { internet: true, allow: ["pypi.org", "*.pythonhosted.org"] }, }); console.log(sbx.id, sbx.info.funding, sbx.info.expiresAt); await sbx.stop(); ``` `timeoutSeconds` is how long the sandbox may run before its lease ends. At the end it pauses (the default) or stops, as `onLeaseEnd` says. `network` narrows what it can reach from its first start; see [the sandbox environment](./sandbox-environment). ## Run commands A string runs under `bash -c`. An array runs the program directly, with no shell, which is what you want for untrusted arguments. ```ts import { Sandbox } from "withruntime"; await using sbx = await Sandbox.create(); await sbx.exec("mkdir -p app && echo 'print(1 + 1)' > app/main.py"); const run = await sbx.exec(["python3", "main.py"], { cwd: "/workspace/app", env: { API_TOKEN: process.env.API_TOKEN ?? "" }, timeoutMs: 120_000, }); if (run.exitCode !== 0) console.error(run.stderr); ``` - `env` is how secrets reach a command. It is never echoed back, and journals record a hash, not the value. Never put a secret in the command line itself. - `stdin` gives the command input, then closes it. - The default timeout is 60 seconds; the maximum is 24 hours. A timeout is a result (`timedOut: true`, with the output so far), not an exception. - `check: true` throws `CommandError` on a non-zero exit, with the result on it. Stream output as it happens with callbacks, or iterate the events: ```ts import { Sandbox } from "withruntime"; await using sbx = await Sandbox.create(); await sbx.exec("for i in 1 2 3; do echo line $i; sleep 1; done", { onStdout: (text) => process.stdout.write(text), onStderr: (text) => process.stderr.write(text), }); for await (const event of sbx.execStream("npm --version")) { if (event.type === "stdout") process.stdout.write(event.data); if (event.type === "exit") console.log("exit", event.exitCode); } ``` A stream that runs past the server's limit resumes by itself from the right byte, so no output is lost or repeated. ## Background processes `spawn` starts a server, a watcher or a REPL and returns at once. The process outlives your connection; get it back later with `sbx.processes.get(id)`. ```ts import { Sandbox } from "withruntime"; await using sbx = await Sandbox.create(); const server = await sbx.spawn("python3 -m http.server 8000", { cwd: "/workspace" }); console.log(server.id, server.info.state); const repl = await sbx.spawn(["python3", "-i", "-q"], { stdin: "pipe" }); await repl.write("print(21 * 2)\n"); await repl.write("exit()\n", { eof: true }); const done = await repl.wait(); console.log(done.stdout); for (const p of await sbx.processes.list()) console.log(p.id, p.state, p.command); await server.kill("SIGTERM"); ``` `process.output()` yields every event from the start, or from a `cursor`, until the process exits. Output is kept in the sandbox, so a reader that reconnects misses nothing. ## An interactive terminal `terminal()` opens a real terminal over a WebSocket: what you write is typed, and `onData` receives what the terminal prints, colours and all. ```ts import { Sandbox } from "withruntime"; await using sbx = await Sandbox.create(); const term = await sbx.terminal({ cols: 120, rows: 40, onData: (bytes) => process.stdout.write(bytes), }); term.write("echo hello from the terminal\n"); term.resize(100, 30); term.write("exit\n"); console.log("exit code", await term.exited); ``` `npx withruntime sandbox shell ` does the same from your own terminal. ## Files Paths are absolute. `/workspace` is the sandbox user's home; any path the user can reach works, and `sudo` reaches the rest. ```ts import { Sandbox } from "withruntime"; await using sbx = await Sandbox.create(); await sbx.files.write("/workspace/data/input.csv", "a,b\n1,2\n"); const text = await sbx.files.readText("/workspace/data/input.csv"); const bytes = await sbx.files.read("/workspace/data/input.csv"); console.log(text.length === bytes.length); console.log(await sbx.files.exists("/workspace/data/input.csv")); console.log(await sbx.files.stat("/workspace/data/input.csv")); for (const entry of await sbx.files.list("/workspace", { depth: 2 })) console.log(entry.type, entry.size, entry.path); console.log(await sbx.files.glob("**/*.csv")); await sbx.files.mkdir("/workspace/out"); await sbx.files.rename("/workspace/data/input.csv", "/workspace/out/input.csv"); await sbx.files.remove("/workspace/data", { recursive: true }); ``` `write` makes parent directories and replaces the file atomically. Large files go in parallel 1 MiB chunks, each checked by SHA-256, and resume after a dropped connection. There is no size limit beyond the disk. Copy whole directories in one call. They travel as one compressed archive: ```ts import { mkdtemp, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { Sandbox } from "withruntime"; const project = await mkdtemp(join(tmpdir(), "project-")); await writeFile(join(project, "main.py"), "print('hi')\n"); await using sbx = await Sandbox.create(); await sbx.files.upload(project, "/workspace/project"); await sbx.exec("cd project && python3 main.py > result.txt"); await sbx.files.download("/workspace/project", join(project, "..", "project-out")); ``` ## Pause, wake, extend ```ts check import { Sandbox } from "withruntime"; const sbx = await Sandbox.create({ timeoutSeconds: 600 }); await sbx.exec("echo state > /workspace/state.txt"); await sbx.pause(); // memory and files are kept; compute billing stops // ... later, even from another process: const again = await Sandbox.connect(sbx.id); await again.wake({ timeoutSeconds: 1200 }); await again.extend(600); // more time before the lease ends await again.stop(); ``` A paused sandbox keeps its memory, its processes and its files. Wake restores it on the same host. See [pricing](./pricing) for what a paused sandbox costs and how long it is kept. ## Find sandboxes again ```ts import { Runtime } from "withruntime"; const runtime = new Runtime(); const page = await runtime.sandboxes.list({ labels: { team: "search" }, state: ["running"] }); for await (const sbx of page) console.log(sbx.id, sbx.info.name, sbx.state); ``` Every list in every product returns a page: `page.data`, `page.hasMore`, `await page.next()` for the next page, `await page.toArray()`, and `for await` walks every item on every page. Filter by `name`, `labels` and `state`; stopped sandboxes are left out unless you pass `includeStopped: true`. ## Errors and retries Every failure is a typed error with a `code`, a `message`, a `hint` that says what to do, and a `requestId` to quote to support. ```ts import { NotFoundError, RuntimeError, Sandbox } from "withruntime"; try { await Sandbox.connect("00000000-0000-4000-8000-000000000000"); } catch (error) { if (error instanceof NotFoundError) console.log("no such sandbox"); else if (error instanceof RuntimeError) console.log(error.code, error.hint, error.requestId); else throw error; } ``` | Class | When | | ------------------------- | ---------------------------------------------------------- | | `AuthenticationError` | 401: the key is missing, wrong or revoked | | `PermissionDeniedError` | 403: the key or account may not do this | | `NotFoundError` | 404: no such resource in this account | | `ConflictError` | 409: the resource is in the wrong state, or the trial busy | | `InvalidRequestError` | 400 and 422: `details` names every wrong field | | `RateLimitError` | 429: slow down; `retryAfterMs` says how long | | `ServiceUnavailableError` | 503: capacity or a dependency; safe to retry | | `ConnectionError` | No answer at all | | `CommandError` | `check: true` and the command did not exit 0 | Every write carries an idempotency key, made for you. Timeouts, dropped connections, 429 and 503 are retried with the same key and a growing delay, so a retried create never makes two sandboxes and a retried command never runs twice. Pass your own `idempotencyKey` to make a retry safe across process restarts. ## Read-only keys and daily limits An owner can make a read-only key, for monitoring and CI, and can set a daily spending limit on any key that spends, at [API keys](https://withruntime.com/account/keys). A key reads both and can change neither. `runtime.limits` needs withruntime 0.3.1 or later: ```ts check import { Runtime } from "withruntime"; const runtime = new Runtime(); const { access, daily } = await runtime.limits.get(); console.log(access); // "full", "read" or "selected" if (daily.remainingMicros !== null && BigInt(daily.remainingMicros) < 1_000_000n) console.log("less than $1 left in this 24-hour window"); ``` Past the limit, a create, wake, extension or renewal fails with a `RuntimeError` whose `code` is `spending_limit_reached` (HTTP 402). It is not retried: stop and tell the person you work for. A read-only key asking to change anything gets `PermissionDeniedError`. See [security](./security). ## Custom images Build an image once with your dependencies, then start every sandbox from it in the same second a plain one takes. Give exactly one source: a `recipe` of packages, a public `image` such as `python:3.12-slim`, or a single-stage `dockerfile` with the files its `COPY` lines read. ```ts check import { Runtime } from "withruntime"; const runtime = new Runtime(); const image = await runtime.images.build( { name: "data", recipe: { pip: ["pandas"], apt: ["jq"] } }, { onLog: (line) => console.log(line.text) }, ); await using sbx = await runtime.sandboxes.create({ image: image.id }); console.log((await sbx.exec("python3 -c 'import pandas; print(pandas.__version__)'")).stdout); ``` `build` waits until the image is ready and throws with the build's own error if it fails; `images.create` queues it and returns at once. An identical recipe is reused instantly. `images.list()`, `images.get(id)`, `images.logs(id)` and `images.delete(id)` do the rest. ## Volumes A volume is a disk that outlives sandboxes. Attach it read-write to one sandbox at a time, or as a read-only `snapshot` copy to any number. ```ts check import { Runtime } from "withruntime"; const runtime = new Runtime(); const volume = await runtime.volumes.create({ sizeMiB: 10_240, name: "cache" }); await using sbx = await runtime.sandboxes.create({ volumes: [{ volumeId: volume.id, path: "/data" }], }); await sbx.exec("sudo chown runtime /data && echo kept > /data/note.txt"); ``` A volume lives on one server and is not backed up off it; a sandbox that uses it is placed on that server. ## Snapshots and forks Forks and snapshots are paused while we fix an issue: for now `fork`, `snapshot` and a create naming `snapshot` answer 503 `fork_unavailable`. Your sandboxes are unaffected. A fork is a copy of a sandbox as it is now: its files, its memory and its running processes, as a new sandbox of its own. Prepare a machine once, then try several things from exactly that point. ```ts check import { Runtime, Sandbox } from "withruntime"; const runtime = new Runtime(); await using base = await Sandbox.create(); await base.exec("pip install --quiet requests"); const [a, b] = await base.fork({ count: 2 }); // both running, answered together await Promise.all([a!.stop(), b!.stop()]); // Or keep the machine to start copies from later: const snapshot = await base.snapshot({ name: "with-requests", retentionDays: 7 }); await using later = await runtime.sandboxes.create({ snapshot: snapshot.id }); await runtime.snapshots.delete(snapshot.id); ``` A running sandbox is paused for the moment a snapshot or fork takes (usually well under a second), then woken; a paused one stays paused. Copies get the source's vCPUs, memory and disk, and run on its host. A snapshot lives on that host and is not copied off it; a sandbox with volumes cannot be snapshotted. ## Code interpreter A notebook-style Python or JavaScript session in the sandbox. Variables persist between runs; charts come back as PNG, data frames as tables. ```ts check import { Sandbox } from "withruntime"; await using sbx = await Sandbox.create(); await sbx.interpreter.run("import math\nx = math.pi"); const cell = await sbx.interpreter.run("round(x * 2, 3)"); console.log(cell.results[0]?.data["text/plain"]); // 6.283 ``` ## Network rules `sbx.network.get()`, `sbx.network.set({ internet, allow, deny, connect })`, `sbx.network.off()` and `sbx.network.on()`; see [the sandbox environment](./sandbox-environment) for what each rule does. ## Share a port A preview gives one port of a sandbox an HTTPS address. It is private by default: a request needs the token, sent as the `x-runtime-preview-token` header, or the one-time `urlWithToken` link for a browser. WebSockets work. ```ts check import { Sandbox } from "withruntime"; await using sbx = await Sandbox.create(); await sbx.spawn("python3 -m http.server 3000"); const preview = await sbx.previews.create(3000); const page = await fetch(preview.url, { headers: { "x-runtime-preview-token": preview.token! }, }); console.log(page.status); ``` Pass `{ visibility: "public" }` for an address anyone can open, `previews.rotate(port)` to refuse every token issued so far, and `previews.delete(port)` to stop sharing. A preview's address is under `runtimehost.com`, the domain for everything sandboxes serve, kept apart from Runtime's own site. ## A desktop A Linux desktop in the sandbox, driven like a person would: open a page, click, type, press keys, take screenshots, and watch it live. ```ts check import { writeFile } from "node:fs/promises"; import { Sandbox } from "withruntime"; await using sbx = await Sandbox.create(); const { streamUrl } = await sbx.desktop.start({ width: 1280, height: 800 }); console.log("watch it:", streamUrl); await sbx.desktop.open("https://example.com"); await sbx.desktop.click(640, 400); await sbx.desktop.type("hello"); await writeFile("screen.png", await sbx.desktop.screenshot()); ``` The live view is a private preview of the desktop: `streamUrl` carries its one-time token, so open it in a browser and keep it to yourself. ## Feedback and support `runtime.feedback.submit(...)` tells the team what broke or is missing, and `runtime.support.message(...)` asks for help; see [feedback and support](./feedback-and-support). ## Configuration ```ts import { Runtime } from "withruntime"; const runtime = new Runtime({ apiKey: process.env.RUNTIME_API_KEY, // the default maxRetries: 4, timeoutMs: 120_000, }); console.log((await runtime.me()).orgId); ``` `RUNTIME_API_URL` points the client at another API origin. Connections are kept alive and reused across calls. Before 0.3.0 the package was `@withruntime/cloud`. That name still installs this package and exports the same classes, so older code and commands keep working.