CWSandbox JS
TypeScript SDK for CoreWeave Sandbox.
This package is the stable Sandbox v1 TypeScript SDK. Ecosystem adapters
are developed in this monorepo and are intended to publish in lockstep after
their initial releases. TanStack and ComputeSDK publishing is currently
deferred; Vercel AI is planned.
This package speaks Sandbox v1. Use services, network.denyEgress /
network.denyIngress, network.egress, runnerIds, and showTerminated.
Profiles, ports, and includeStopped are not part of this API. Use
runFromTemplate / withSandboxFromTemplate for organization templates.
Use runFromFile / withSandboxFromFile for Compose files.
For platform concepts and product guides, see the
CoreWeave Sandbox documentation.
Install And Prerequisites
This package supports Node.js >=22 and ESM projects. CI specifically validates
Node.js 22 and 24 (LTS) plus Node.js 26 (Current). The matrix adds each new
Current release, retains active LTS releases, and removes versions at Node EOL.
npm install @coreweave/cwsandbox
Other package managers:
pnpm add @coreweave/cwsandbox
yarn add @coreweave/cwsandbox
Use an API key with the Node gRPC client:
export CWSANDBOX_API_KEY="..."
export CWSANDBOX_BASE_URL="https://api.cwsandbox.com"
CWSANDBOX_BASE_URL defaults to https://api.cwsandbox.com. If CWSANDBOX_API_KEY
is missing or blank, createSandboxClientFromEnv() throws CWSandboxConfigurationError.
To authenticate through the W&B sandbox gateway, use the W&B wrapper subpath:
export WANDB_API_KEY="..."
export WANDB_ENTITY="my-team"
export WANDB_PROJECT="sandbox"
export WANDB_SANDBOX_BASE_URL="..."
import { createSandboxClientFromEnv } from "@coreweave/cwsandbox/wandb";
const client = createSandboxClientFromEnv();
W&B auth resolves credentials in order: explicit apiKey, WANDB_API_KEY, then
the password for api.wandb.ai or wandb.ai in ~/.netrc. The W&B wrapper sends
x-wandb-api-key, x-cwsandbox-client-version, x-wandb-sdk-version, optional
entity/project headers, and x-sandbox-integration: js-sdk to the sandbox gateway.
Both version headers use this package's version for now.
Entrypoints
The root package is transport-neutral and contains public types, errors, and client interfaces:
import { DEFAULT_KEEP_ALIVE_COMMAND } from "@coreweave/cwsandbox";
import type { SandboxClient } from "@coreweave/cwsandbox";
Node gRPC helpers live under the Node entrypoint:
import { DEFAULT_CONTAINER_IMAGE } from "@coreweave/cwsandbox/node";
import { createSandboxClientFromEnv } from "@coreweave/cwsandbox/node";
createSandboxClientFromEnv() reads CWSANDBOX_API_KEY and optional CWSANDBOX_BASE_URL.
DEFAULT_KEEP_ALIVE_COMMAND keeps a sandbox available for multiple operations and exits cleanly when stopped; client.create() uses it by default.
When containerImage is omitted, the Node transport uses DEFAULT_CONTAINER_IMAGE (python:3.11).
The W&B wrapper subpath exposes W&B-native factory names:
import { createSandboxClient } from "@coreweave/cwsandbox/wandb";
const client = createSandboxClient({
apiKey: "...",
entity: "my-team",
project: "sandbox",
});
This subpath is intentionally a lightweight proof point for W&B gateway auth. A
future W&B SDK wrapper can resolve logged-in W&B credentials automatically and add
W&B Serverless policy guardrails. For now, avoid runner/profile placement
overrides, GPU resource requests, and unsupported egress modes when using the W&B
gateway path.
Adapter Packages
Additional ecosystem adapters are sibling workspace packages in this monorepo so
they can carry their own peer dependencies and compatibility tests.
@coreweave/cwsandbox-tanstack adapts this SDK to TanStack AI's
SandboxProvider contract for defineSandbox(...) / withSandbox(...)
workflows (same lockstep version; private until its fast-follow publish).
@coreweave/cwsandbox-computesdk adapts this SDK to ComputeSDK's
defineProvider contract via the coreweave(...) factory (same lockstep
version; public package metadata, npm publish deferred).
Examples
Runnable recipes live under examples/. See
examples/README.md for the full gallery (SDK scripts
in examples/sdk/, plus Weave and TanStack integrations) and the local list of
Python examples not yet matched in JS.
Quick start:
pnpm --dir examples/sdk quick-start
Typecheck the SDK recipe package without creating a live sandbox:
pnpm --dir examples/sdk typecheck
Integration examples:
pnpm --dir examples/weave start
pnpm --dir examples/tanstack start
pnpm --dir examples/weave typecheck
pnpm --dir examples/tanstack typecheck
API Map
- Construct clients only through the Node/W&B factories (
createSandboxClient,
createSandboxClientFromEnv). Direct construction and transport replacement are not
supported public APIs.
SandboxClient creates, reconnects, lists, and deletes sandboxes.
createSandboxClientFromEnv() in @coreweave/cwsandbox/node wires the Node gRPC transport from environment variables.
client.withSandbox(callback, options) runs short-lived work in a ready sandbox with automatic cleanup.
client.create(options) starts a long-lived ready sandbox you manage explicitly.
client.run(command, options) starts a sandbox with a custom main process.
sandbox.commands.run(...) buffers command output.
sandbox.commands.start(...) streams command output and optionally accepts stdin.
sandbox.files.* reads and writes sandbox files (readStream / writeStream for incremental transfers).
sandbox.logs.* reads or streams the sandbox main process logs.
sandbox.wait(...), sandbox.stop(...), and sandbox.delete(...) manage lifecycle.
stop() requests shutdown and waits until the sandbox is terminal; use
wait({ targetStatus: "terminal" }) to observe completion without sending Stop.
sandbox.snapshot() archives the scratch volume and waits until READY (default 600s).
Snapshots outlive sandbox stop/delete; client.deleteSnapshot(id, { missingOk: true })
removes them.
Direct sandbox connections
Exec, shell, log, and file operations prefer a sandbox-scoped direct mTLS
connection to the runner by default. Sandbox creation, inspection, snapshots,
stop, and delete always use the CoreWeave Sandbox API.
import { createSandboxClientFromEnv } from "@coreweave/cwsandbox/node";
const client = createSandboxClientFromEnv();
const sandbox = await client.create({ dataPlaneMode: "auto" });
try {
const result = await sandbox.commands.run(["python", "-c", "print('direct when available')"]);
console.log(result.stdout);
} finally {
await sandbox.stop();
}
dataPlaneMode supports:
"auto" (default): try direct for up to one second, then use the gateway.
"direct": require direct mTLS and return an unavailable error if it cannot
be established.
"gateway": route all data operations through the gateway.
Set a client-wide default with
createSandboxClient({ apiKey, dataPlaneMode: "gateway" }), or set the mode on
create, run, runFromTemplate, runFromFile, fromId, or listSandboxes. Per-sandbox
options override the client default. Direct credentials are requested lazily,
kept in memory, scoped to one sandbox and operation, and expire with the
server-issued certificate. Direct calls do not send the API bearer token.
Quickstart
Prefer withSandbox() for short-lived work. It starts a sandbox, passes it to your callback, and stops it when the callback finishes.
import { createSandboxClientFromEnv } from "@coreweave/cwsandbox/node";
const client = createSandboxClientFromEnv();
const result = await client.withSandbox(async (sandbox) => {
return sandbox.commands.run(["python", "-c", "print('hello from cwsandbox-js')"]);
});
console.log(result.stdout);
Use create() directly when you need to keep a sandbox across multiple operations:
import { createSandboxClientFromEnv } from "@coreweave/cwsandbox/node";
const client = createSandboxClientFromEnv();
const sandbox = await client.create();
try {
const result = await sandbox.commands.run(["python", "-c", "print('hello')"]);
console.log(result.stdout);
} finally {
await sandbox.stop();
}
await sandbox.stop() is Stop-then-wait: it sends the Stop RPC (unless the sandbox is
already terminating or terminal), then polls until a terminal status. Concurrent or
repeated stop() calls on the same handle share one in-flight operation. Per-call
signal / timeoutMs only bound that waiter’s await; they do not cancel shared shutdown
work for other waiters, and aborting does not undo a Stop that already succeeded.
After a successful Stop, a brief NotFound race is retried (~2s). If terminal status is
still unobservable, stop() throws CWSandboxTerminalStateUnavailableError.
stop()'s status check and its Stop RPC each retry a transient UNAVAILABLE that carries
a server RetryInfo delay of at most 10s: up to 3 calls per step, so up to 6 calls and
about 48s of backoff before polling starts. These shared steps get no deadline, and a
waiter's timeoutMs / signal still only bound that waiter. If a retried Stop finds the
sandbox gone, it is already stopped: stop() resolves with status terminated and
does not poll. If the shared stop operation still fails, that failure is cached on the
handle; retry with a fresh handle from client.fromId() or use delete().
To watch an already-stopping sandbox without sending Stop:
await sandbox.wait({ targetStatus: "terminal", timeoutMs: 60_000 });
console.log(sandbox.status);
console.log(sandbox.exitCode);
wait() still defaults to a 60s timeout (including targetStatus: "terminal").
stop()’s shared wait is unbounded unless a waiter passes timeoutMs.
Default wait-until-running (wait() / create/run with waitUntilRunning: true)
treats paused as ready (same as running). If the sandbox reaches completed
during startup, wait resolves successfully. failed and terminated raise
CWSandboxFailedError and CWSandboxTerminatedError. A terminating status is
polled through until a real terminal outcome. Explicit targetStatus values:
paused and completed stay exact-match; terminal still means any terminal
status (completed / failed / terminated).
Status polling uses an internal backoff (about 200ms toward 2s). Transient Get
failures (unavailable, request deadline, resource_exhausted) are retried within
an internal ~30s budget; the wait’s absolute timeoutMs deadline also clamps that
burst so retries cannot overrun the waiter. NOT_FOUND is not retried on
observe-only waits. When the server includes AIP-193 RetryInfo, the SDK honors
retryDelayMs (capped at 10s). If wait observes completed without exitCode, it
makes up to two short extra Gets (~2s apart) so a late runner stamp can land;
client stop() skips that window because gateway-initiated stops never stamp a
code. Poll pacing and retry budget are not public options — bound waits with
timeoutMs / signal / targetStatus (the former fixed intervalMs wait option
is removed in beta; Python has no wait poll-interval knob either).
Modern runtimes can also use explicit resource management:
import { createSandboxClientFromEnv } from "@coreweave/cwsandbox/node";
const client = createSandboxClientFromEnv();
await using sandbox = await client.create();
const result = await sandbox.commands.run(["python", "-c", "print('hello')"]);
console.log(result.stdout);
client.create(), client.run(...), client.runFromTemplate(...), client.runFromFile(...),
client.withSandbox(...), client.withSandboxFromTemplate(...), and
client.withSandboxFromFile(...) wait for the sandbox to reach
running by default, so the returned sandbox is safe for exec, file, and log operations. This is
sandbox lifecycle readiness, not application readiness: if your main process starts an HTTP server
or performs setup, wait for that app-specific condition with commands, logs, files, or services.
Pass waitUntilRunning: false when you need a handle immediately after the backend accepts the
start request:
const sandbox = await client.create({ waitUntilRunning: false });
await sandbox.wait({ timeoutMs: 30_000 });
Use run() when the sandbox main process matters, for example to stream logs from PID 1:
const sandbox = await client.run(["python", "-m", "http.server", "8000"], {
services: [{ port: 8000 }],
});
Commands
sandbox.commands.run() executes a command and returns buffered stdout/stderr plus an exit code.
sandbox.exec() is a direct alias.
const result = await sandbox.commands.run(["python", "-c", "print('ok')"]);
if (result.exitCode !== 0) {
console.error(result.stderr);
}
Use cwd for a working directory:
await sandbox.commands.run(["pwd"], { cwd: "/tmp" });
Use commands.start() when you need to stream output while a command is running:
const process = await sandbox.commands.start(["pytest", "-q"], {
cwd: "/workspace",
});
for await (const chunk of process.stdout) {
console.log(chunk);
}
const result = await process.wait();
console.log(process.status);
console.log(result.exitCode);
console.log(result.ok);
commands.start() is text-oriented (stdout / stderr are string streams). For
binary file transfers, use files.readStream / files.writeStream instead of
command stdout.
process.poll() returns undefined while the command is still running, then the exit code after completion. process.exitCode is also populated after the command exits.
process.wait({ timeoutMs, signal }) can bound how long you wait locally without changing the running command.
Enable stdin when the command needs input. With { stdin: true }, TypeScript returns a process with a non-optional stdin writer:
const process = await sandbox.commands.start(["cat"], {
stdin: true,
});
await process.stdin.writeln("hello");
await process.stdin.close();
const result = await process.wait();
console.log(result.stdout);
Interactive Shell
Use sandbox.shell() for TTY sessions that need terminal semantics such as ANSI
escape sequences, shell prompts, and resize events. TTY output is raw bytes with
stdout and stderr merged, so decode it only when you want text:
const terminal = await sandbox.shell({
command: ["/bin/sh"],
cols: 80,
rows: 24,
});
const output = (async () => {
for await (const chunk of terminal.output) {
process.stdout.write(chunk);
}
})();
await terminal.stdin.writeln("echo hello from tty");
await terminal.resize(120, 40);
await terminal.stdin.writeln("exit 0");
await terminal.stdin.close();
const result = await terminal.wait();
await output;
console.log(result.exitCode);
The default shell command is ["/bin/bash"]. Command resume is not part of the
initial shell API.
wait() returns accumulated output plus convenience result helpers:
const result = await sandbox.commands.run(["python", "-c", "import sys; sys.exit(1)"]);
if (result.failed) {
console.error(result.stderr);
}
Use check: true when non-zero process exits should throw CWSandboxExecutionError.
The error includes the full ProcessResult:
import { CWSandboxExecutionError } from "@coreweave/cwsandbox";
try {
await sandbox.commands.run(["pytest", "-q"], { check: true });
} catch (error) {
if (error instanceof CWSandboxExecutionError && error.result !== undefined) {
console.error(error.result.exitCode);
console.error(error.result.stderr);
} else {
throw error;
}
}
Use bufferedMaxKiB with commands.start() to cap the final accumulated stdout / stderr stored on the result. Live streamed chunks are still delivered as they arrive:
const process = await sandbox.commands.start(["pytest", "-q"], {
bufferedMaxKiB: 1024,
});
ProcessResult also includes stdoutBytes, stderrBytes, numeric stdoutBytesProduced / stderrBytesProduced, and truncation booleans. Streaming chunks are text chunks, not guaranteed lines. stdout and stderr are single-consumer async iterables; consume them while the command runs if you need every live chunk. wait() remains reliable even when streams are not consumed. Non-zero process exits resolve through wait() with an exit code by default, or throw CWSandboxExecutionError when the command was started with check: true; transport failures always throw SDK errors.
process.cancel() cancels the client-side streaming call. It is not named kill() because the current streaming protocol does not expose a remote process signal contract. TTY and command resume are future features.
Logs
The logs namespace streams stdout/stderr from the sandbox main command passed to client.run(). Output from commands.run() and commands.start() is not included in sandbox logs.
const lines = await sandbox.logs.read({ tailLines: 100 });
console.log(lines.join(""));
Follow logs like tail -f and close the stream when you are done:
const logs = await sandbox.logs.stream({ follow: true, tailLines: 10 });
try {
for await (const line of logs) {
process.stdout.write(line);
if (line.includes("READY")) {
await logs.close();
}
}
} finally {
await logs.close();
}
Use sinceTime and timestamps for bounded reads:
const recent = await sandbox.logs.read({
sinceTime: new Date(Date.now() - 60_000),
timestamps: true,
});
Advanced log APIs expose cursor metadata and raw backend chunks:
for await (const entry of await sandbox.logs.streamEntries({ follow: true })) {
console.log(entry.offset, entry.line);
}
for await (const chunk of await sandbox.logs.streamRaw({ tailLines: 1 })) {
console.log(chunk.data.byteLength, chunk.text);
}
Resume is explicit and caller-controlled:
const resumed = await sandbox.logs.stream({
follow: true,
resume: { offset: "128", sessionId: "session-123" },
});
await resumed.cancel();
The default keep-alive command is silent. Log streams are line-oriented; streamRaw() is available when you need exact backend chunk boundaries.
Files
The files namespace supports string and Uint8Array content.
await sandbox.files.write("/tmp/hello.txt", "hello");
const text = await sandbox.files.readText("/tmp/hello.txt");
const bytes = await sandbox.files.read("/tmp/hello.txt");
Buffered vs streaming
files.read / files.write | Fully buffered Uint8Array / string | Small–medium files |
| Auto StreamExec fallback (#9) | Still buffered end-to-end | Mid-size unary overflow up to ~256 MiB |
files.readStream / files.writeStream | Incremental Uint8Array chunks | Large files; escape hatch past the 256 MiB buffered cap |
Payloads up to roughly 32 MiB use unary file RPCs. Larger payloads (up to
256 MiB) automatically fall back to a single StreamExec (sh + cat) path,
matching the Python SDK. Writes above 256 MiB (and oversized reads that are not
auto-fallback candidates) are refused on the buffered APIs with
CWSandboxFileError and reason CWSANDBOX_FILE_TOO_LARGE — use
writeStream / readStream instead. The buffered StreamExec auto-fallback can
still OOM (exit 137) on larger mid-size payloads today — the same environmental
limit as Python; incremental streaming avoids accumulating the full payload in
the SDK.
await sandbox.files.writeStream("/tmp/big.bin", new Uint8Array(1024));
await sandbox.files.writeStream("/tmp/chunks.bin", [
new Uint8Array([1, 2]),
new Uint8Array([3, 4]),
]);
let total = 0;
for await (const chunk of sandbox.files.readStream("/tmp/big.bin")) {
total += chunk.byteLength;
}
console.log(total);
Notes for streaming:
- Mid-failure or
signal abort on writeStream may leave a partial remote file.
- Early stop / abort on
readStream best-effort cancels the StreamExec process.
readStream({ timeoutMs }) is one wall-clock across the integrity stat and
the file transfer. The clock starts when iteration begins. Omit timeoutMs
for an unbounded transfer (stat is still capped internally at 10s).
- Slow work inside the read loop can trip
CWSandboxStreamBackpressureError
(STREAM_BACKPRESSURE); drain first, process afterward.
- Bad iterable chunks (not
Uint8Array) throw CWSandboxValidationError.
The buffered methods also accept batch inputs:
await sandbox.files.write({
"/tmp/a.txt": "hello",
"/tmp/b.bin": new Uint8Array([1, 2, 3]),
});
const texts = await sandbox.files.readText(["/tmp/a.txt"]);
const files = await sandbox.files.read(["/tmp/b.bin"]);
console.log(texts["/tmp/a.txt"]);
console.log(files["/tmp/b.bin"]);
Start Options
Mounted Files
Use record form for concise text or byte mounts:
const sandbox = await client.run(["python", "/workspace/main.py"], {
mountedFiles: {
"/workspace/main.py": "print('hello from mounted file')",
},
});
Use array form when you prefer explicit objects:
await client.run(["python", "/workspace/main.py"], {
mountedFiles: [
{
path: "/workspace/main.py",
content: "print('hello')",
},
],
});
File-system snapshots
fileSystemSnapshot mounts one scratch volume named workspace at mountPath.
Snapshots archive that mount, not the whole container overlay.
const source = await client.create({
fileSystemSnapshot: {
mountPath: "/workspace",
size: "10Gi",
},
});
await source.exec(["sh", "-c", "echo hello > /workspace/data.txt"]);
const { snapshotId, sizeBytes, state, objectBucket } = await source.snapshot();
await source.delete({ missingOk: true });
const inspected = await client.getSnapshot(snapshotId);
const listed = await client.listSnapshots({ sourceSandboxId: source.sandboxId });
const restored = await client.create({
fileSystemSnapshot: {
mountPath: "/workspace",
restoreFromSnapshotId: snapshotId,
},
});
const result = await restored.exec(["cat", "/workspace/data.txt"]);
console.log(result.stdout, sizeBytes, state, objectBucket, listed.length, inspected.state);
await restored.delete({ missingOk: true });
await client.deleteSnapshot(snapshotId, { missingOk: true });
snapshot() waits until READY or FAILED and returns the READY Get record
(state, trigger, optional objectBucket / timestamps), not only the ID.
Python snapshot() returns the ID; call get_snapshot there for the record.
The public default wait is 600s (plus 5s internal observation slack). Pass
timeoutMs to override the archive budget. Snapshots are not deleted when the
sandbox stops; call deleteSnapshot. Inspect without waiting with
client.getSnapshot(snapshotId) and client.listSnapshots({ sourceSandboxId, state }).
listSnapshots collects every page and filters client-side (it does not send
sourceSandboxId on the List RPC).
fileSystemSnapshot and volumes cannot be used together. For a named mount
or more than one scratch, pass volumes. snapshot() cannot choose among
multiple scratches created in this process.
await client.create({
volumes: [
{ name: "workspace", mountPath: "/workspace", size: "10Gi" },
{ name: "cache", mountPath: "/cache" },
],
});
stop({ snapshotOnStop }) is not supported. Capture with sandbox.snapshot()
before stop or delete.
Object storage access
Mint temporary object-storage credentials for the sandbox at create time
(independent of snapshots):
await client.create({
objectStorageAccess: {
buckets: ["example-bucket"],
permission: "read-write",
objectPrefix: "tenants/org-abc/cache/",
},
});
objectPrefix is optional. When set it must start alphanumeric, end with /,
and must not contain .. or //.
Resources
Flat CPU/memory resources map to guaranteed requests:
await client.run(["python"], {
resources: {
cpu: "2",
memory: "4Gi",
},
});
Use requests/limits for burstable CPU and memory:
await client.run(["python"], {
resources: {
requests: { cpu: "1", memory: "1Gi" },
limits: { cpu: "4", memory: "8Gi" },
},
});
Tags
Tags are useful for discovery and cleanup:
const tags = ["project-demo", "purpose-smoke"] as const;
const sandbox = await client.create({ tags });
const listed = await client.list({ tags: ["project-demo"] });
Tags may contain letters, numbers, ., _, or -, must be 59 characters or fewer, and must end with a letter or number.
Annotations
Annotations are non-sensitive infrastructure metadata for the sandbox pod:
await client.run(["python"], {
annotations: {
team: "platform",
purpose: "smoke-test",
},
});
Do not put secrets in annotations. Use secrets for store-backed injection.
Secrets
Pass secret-store references at create/run time. The gateway resolves them
server-side and injects the values as environment variables. The client never
sends secret values.
Field names match the Python SDK Secret (store, name, field, env_var),
with camelCase envVar for TypeScript. On the wire, name is sent as proto
SecretMapping.path. At most 50 secrets may be referenced per sandbox (Gateway
pre-resolve limit).
await client.create({
secrets: [
{ store: "wandb-team-secrets", name: "HF_TOKEN" },
{
store: "wandb-team-secrets",
name: "db-credentials",
field: "password",
envVar: "DB_PASS",
},
],
});
store must match a Gateway-registered secret store name for the organization.
For W&B team secrets this is typically wandb-team-secrets.
name is the secret id in that store (proto path).
field is optional for structured secrets.
envVar defaults to name when omitted.
For W&B-backed stores, authenticate through the W&B client path
(@coreweave/cwsandbox/wandb) so identity claims can resolve team secrets.
Create the secret in the W&B team Secret Manager first; registering the org
secret store on Gateway is a one-time admin step.
Do not put secret values in environmentVariables, annotations, or tags.
Network And Services
Internet egress follows the fleet policy default. Deny outbound or inbound
traffic with boolean flags. denyIngress only affects CUSTOM-visibility ports
and is a no-op when the sandbox has none — it does not hide a public HTTPS
endpoint:
await client.run(["python"], {
network: {
denyEgress: true,
},
});
await client.run(["python"], {
network: {
denyIngress: true,
},
});
Grant specific hostnames over HTTPS (TCP 443). A one-label wildcard
(*.pypi.org) does not include the apex (pypi.org); grant both when the
example needs PyPI. "*" is a policy ceiling, not a sandbox grant, and
denyEgress: true cannot combine with a non-empty egress list:
const sandbox = await client.run(["python"], {
network: {
egress: [{ dnsName: "pypi.org" }, { dnsName: "*.pypi.org" }],
},
});
console.log(sandbox.dnsEgressNames);
console.log((await sandbox.inspect()).dnsEgressNames);
Declare listen-only services, or request a public HTTPS assignment with
endpoint: { kind: "https", auth: "open" | "share_token" } and
visibility: "public".
Optional requestTimeoutSeconds is the server-side HTTPS request clock (504
while the sandbox stays alive). Omit or 0 keeps the platform default (15s
on serverless). This is not timeoutMs on client.run / RPCs:
await client.run(["python", "-m", "http.server", "8000"], {
services: [{ port: 8000 }],
});
const sandbox = await client.run(["python", "-m", "http.server", "8000"], {
services: [
{
endpoint: { auth: "open", kind: "https", requestTimeoutSeconds: 120 },
name: "http",
port: 8000,
visibility: "public",
},
],
});
const info = await sandbox.inspect();
console.log(info.serviceUrls?.[0]?.url);
Use auth: "share_token" when the URL should require a platform credential.
Create returns endpointShareToken once; Get, list, and fromId omit it. A
live handle keeps a create-time token across wait / inspect. Send it as
X-Sandbox-Share-Token (query ?share_token= is a fallback for clients that
cannot set headers). If create succeeds without both a URL and a token, delete
and recreate — Get cannot recover the token. Log the URL only, never the raw
token:
const sandbox = await client.run(["python", "-m", "http.server", "8000"], {
services: [
{
endpoint: { auth: "share_token", kind: "https" },
name: "http",
port: 8000,
visibility: "public",
},
],
});
const url = sandbox.serviceUrls?.[0]?.url;
const token = sandbox.endpointShareToken;
if (!url || !token) {
throw new Error(
"Share-token URL/token missing on create. Delete and recreate; Get/fromId cannot recover the token.",
);
}
console.log(`URL: ${url}; share token: received`);
const ok = await fetch(url, {
headers: { "X-Sandbox-Share-Token": token },
});
A sandbox may mix open and share-token HTTPS rows. One token covers every
share-token row. Open rows do not need it.
A non-empty serviceUrls entry means the hostname was assigned. That is not
the same as the application listening, and not the same as the edge being
ready. Applied timeout is not echoed on serviceUrls. When the API applied a
timeout (requestTimeoutSeconds > 0), inspect and list echo it on
serviceEndpoints, including url: "" on a terminal Get that suppressed the
hostname. Those timeout rows stay off serviceUrls unless a hostname was
assigned.
Request TLS passthrough with endpoint: { kind: "tls_passthrough" } on a
PUBLIC service. Omit auth and requestTimeoutSeconds. The assigned target
is host:port on serviceAddresses. Use the host as TLS SNI. The workload
owns certs. TLS addresses stay off serviceUrls:
const sandbox = await client.run(["node"], {
containerImage: "node:22",
services: [
{
endpoint: { kind: "tls_passthrough" },
name: "tls",
port: 8443,
visibility: "public",
},
],
});
console.log(sandbox.serviceAddresses?.[0]?.address);
A non-empty serviceAddresses entry means the target was assigned, not that
the application or edge is ready. On a live handle, a later CREATING/RUNNING
Get keeps a cached address per (port, name) when that service is still
present and Get omits the endpoint or address. Wire STATE_PREPARING maps to
creating, so a live handle keeps the address when service rows remain
(visible on Get/poll). fromId and list have no Create cache. Any other
status, including paused and unspecified, clears it.
Sandbox handles expose cached backend metadata. Use inspect() when you need a
fresh one-shot metadata snapshot for traces, tool results, or logs:
const info = await sandbox.inspect();
const sandboxTrace = {
sandboxId: info.sandboxId,
status: info.status,
exitCode: info.exitCode,
startedAt: info.startedAt?.toISOString(),
serviceUrls: info.serviceUrls,
serviceEndpoints: info.serviceEndpoints,
serviceAddresses: info.serviceAddresses,
runnerId: info.runnerId,
statusReason: info.statusReason,
};
console.log(sandboxTrace);
Placement Selectors
Pin a sandbox to specific CKS runners with runnerIds. Profile selectors are
not supported in v1; use runFromTemplate when you need that style of
placement:
await client.run(["python"], {
runnerIds: ["runner-1"],
});
Templates
runFromTemplate(templateId, options?) starts a sandbox from an organization
template. Omitted options keep template values. Empty tags: [], services: [],
annotations: {}, and runnerIds: [] mean inherit, not clear — there is no
clear-to-empty operation. Supplying containerImage replaces the entire
container list (omitted container settings, including private-image credentials,
are not inherited). Field-level replacement details live on
SandboxRunFromTemplateOptions.
If creation returns an accepted sandbox but the readiness wait rejects, the
SDK best-effort stops it. waitUntilRunning: false returns immediately after
accept with no automatic cleanup. create / run do not yet follow this
readiness-failure cleanup behavior.
placementMode is not available; CKS placement is only via non-empty
runnerIds. ResourceOptions is CPU and memory only (no GPU). secrets: []
requires containerImage. volumes: [] is rejected. snapshot() with multiple
inherited scratches is a backend error; one inherited scratch can infer the
volume.
Prefer withSandboxFromTemplate for short-lived work. Use await using with
runFromTemplate for a direct handle with scoped cleanup:
import { DEFAULT_KEEP_ALIVE_COMMAND } from "@coreweave/cwsandbox/node";
const inherited = await client.withSandboxFromTemplate(
"template-id",
async (sandbox) => sandbox.sandboxId,
{ tags: ["example"] },
);
await using replaced = await client.runFromTemplate("template-id", {
containerImage: "python:3.11",
command: DEFAULT_KEEP_ALIVE_COMMAND,
tags: ["example"],
});
await replaced.inspect();
console.log(inherited, replaced.sandboxId);
Compose Files
runFromFile(contents, options) starts a sandbox from Compose YAML. contents
is a filesystem path or raw file bytes. A string is always opened as a path;
encode Compose text with new TextEncoder().encode(...). Bytes are sent as-is,
so whitespace is part of the request. The file is capped at 256 KiB.
primaryService is required and must name a service in the file. Optional
imageOverrides and CPU/memory defaultResources apply per service. Images
must already be pullable; leftover build: is not supported. Volumes,
published services, secrets, mounted files, and container overlays stay on
create / run.
Wait for running as usual. If creation is accepted but the readiness wait
rejects, the SDK best-effort stops the sandbox. waitUntilRunning: false
returns immediately after accept with no automatic cleanup.
Prefer withSandboxFromFile for short-lived work:
const contents = new TextEncoder().encode(`services:
main:
image: docker.io/library/ubuntu:22.04
command: ["sleep", "3600"]
`);
await client.withSandboxFromFile(
contents,
async (sandbox) => {
const result = await sandbox.commands.run(["uname", "-s"]);
console.log(result.stdout);
},
{
primaryService: "main",
tags: ["example"],
},
);
Reconnect, List, And Delete
Get fresh sandbox metadata without creating a sandbox handle:
const info = await client.get("sandbox-id");
console.log(info.status);
Reconnect to an existing sandbox:
const sandbox = await client.fromId("sandbox-id");
Use fromId() when you need to run commands, read files, stream logs, or manage lifecycle through a Sandbox instance. Use get() when you only need current metadata.
List sandboxes — most callers want every active match as usable handles.
Listing defaults to active-only (showTerminated is false):
const sandboxes = await client.listAll({
tags: ["project-demo"],
pageSize: 25,
});
await Promise.all(sandboxes.map((sandbox) => sandbox.delete()));
listAll() is an alias of listSandboxes(...).collect(). Both return Sandbox instances built from list metadata (no extra RPCs until you call methods on a handle). list() returns one page of SandboxInfo metadata plus an optional nextPageToken if you are managing pagination yourself. On the helpers, timeoutMs is a wall-clock budget across all pages (default 300 seconds), not a per-page RPC timeout.
Stream sandboxes as pages arrive:
for await (const sandbox of client.listSandboxes({ tags: ["project-demo"], pageSize: 25 })) {
await sandbox.delete();
}
Process page batches:
for await (const page of client.listSandboxes({ tags: ["project-demo"], pageSize: 25 }).byPage()) {
await Promise.all(page.map((sandbox) => sandbox.delete()));
}
One page at a time (manual pagination):
const { sandboxes, nextPageToken } = await client.list({
tags: ["project-demo"],
pageSize: 25,
});
Delete through the client or sandbox instance:
await client.delete("sandbox-id");
await sandbox.delete();
By default, deleting a missing sandbox raises CWSandboxNotFoundError. Pass
missingOk: true for cleanup scripts that should treat “already gone” as
success (same for stop({ missingOk: true })):
await client.delete("sandbox-id", { missingOk: true });
await sandbox.stop({ missingOk: true });
await client.deleteSnapshot("snapshot-id", { missingOk: true });
Clean up interrupted active work by listing with the same tags you used at
start. Listing defaults to active-only. Do not pass showTerminated: true
here — that flag includes terminal rows, it does not mean “stopped only”:
const sandboxes = await client.listAll({
tags: ["project-demo"],
});
await Promise.all(sandboxes.map((sandbox) => sandbox.delete({ missingOk: true })));
Error Handling
All SDK errors extend CWSandboxError and expose a stable code string.
Transport failures may also carry AIP-193 fields when the backend includes
google.rpc.ErrorInfo / RetryInfo in gRPC status details:
reason — branch key (e.g. CWSANDBOX_SANDBOX_NOT_FOUND)
domain — namespace; reason→class mapping only applies for cwsandbox.com
metadata — ErrorInfo metadata map (always an object; empty when absent)
retryDelayMs — optional RetryInfo hint
delete() and files.read() through the Gateway retry a transient failure before
throwing, but only when the server sends gRPC UNAVAILABLE with a RetryInfo delay of
at most 10s. Each makes up to 3 calls, waiting the server's delay plus up to 20%. With a
timeoutMs, the retries stay within it and need at least 5s left for the next call;
without one, no new deadline is added. Aborting the signal during a wait stops further
calls. A not-found answer on a retried delete counts as deleted. stop() applies the
same retry to its status check and Stop RPC, without the caller's timeout or signal (see
the stop section above). Other operations (exec, streams, writes, create, list, and other
gets) do not add this retry. When the retries run out, the last error is thrown.
import { CWSANDBOX_FILE_TOO_LARGE } from "@coreweave/cwsandbox";
import { CWSandboxNotFoundError } from "@coreweave/cwsandbox";
import { CWSandboxStreamBackpressureError } from "@coreweave/cwsandbox";
import { CWSandboxTimeoutError } from "@coreweave/cwsandbox";
import { CWSandboxTransportError } from "@coreweave/cwsandbox";
import { CWSandboxUnavailableError } from "@coreweave/cwsandbox";
import { CWSandboxValidationError } from "@coreweave/cwsandbox";
import { isCWSandboxError } from "@coreweave/cwsandbox";
try {
await sandbox.wait({ timeoutMs: 10_000 });
} catch (error) {
if (error instanceof CWSandboxTimeoutError) {
console.error("Sandbox did not become ready in time.");
} else if (error instanceof CWSandboxUnavailableError) {
console.error("Sandbox service is temporarily unavailable.");
} else if (error instanceof CWSandboxNotFoundError) {
console.error("Sandbox no longer exists.");
} else if (error instanceof CWSandboxStreamBackpressureError) {
console.error("Drain streams faster or use files.readStream / writeStream.");
} else if (
error instanceof CWSandboxTransportError &&
error.reason === CWSANDBOX_FILE_TOO_LARGE
) {
console.error("File too large for unary path; use streaming.");
} else if (error instanceof CWSandboxValidationError) {
console.error(error.message);
} else if (isCWSandboxError(error)) {
console.error(error.code, error.message);
} else {
throw error;
}
}
Transport errors may also include operation, sandboxId, transport, and
transportCode for logging. Raw gRPC trailing metadata stays on error.cause
when the failure came from the Node gRPC transport.
Testing Without Credentials
Factories are the only supported creation path. Transport replacement and constructing
SandboxClient / Sandbox implementations directly are not supported public APIs.
To unit test application code, prefer supplying a SandboxClient interface fake to the
code under test (not a transport). Alternatively, use the Node factory with a mock API
key and intercept network calls.
For an example of how the package itself tests with fake implementations, see
src/transport.contract.test.ts in the source tree.
Environment
Copy .env.example for local experiments:
CWSANDBOX_API_KEY=
CWSANDBOX_BASE_URL=https://api.cwsandbox.com
WANDB_API_KEY=
WANDB_ENTITY=
WANDB_PROJECT=
WANDB_SANDBOX_BASE_URL=
Do not put secret values in environmentVariables, annotations, or tags.
Use secrets for store-backed injection (see Secrets).
Development
This package lives in a pnpm monorepo. From the repository root:
pnpm install
pnpm check
Useful root commands:
pnpm test runs core unit tests.
pnpm test:types runs public API type tests.
pnpm test:readme typechecks TypeScript examples in this README.
pnpm test:package builds the package and checks real package exports from fixture consumers.
pnpm format:fix applies Oxfmt formatting.
pnpm lint:fix applies Oxlint fixes.
pnpm fix runs lint fixes and formatting.
pnpm smoke runs the credential-gated live e2e smoke suite, including W&B auth when WANDB_API_KEY or a W&B .netrc entry is available.
pnpm smoke:stress runs the credential-gated standard stress smoke suite.
pnpm smoke:stress -- --heavy runs the larger manual stress smoke suite.
pnpm smoke:stress -- --cleanup --tag <stress-tag> deletes sandboxes from an interrupted stress run.
pnpm check is offline and credential-free, including README example typechecks. pnpm smoke and stress smoke commands skip CoreWeave-auth tests when CWSANDBOX_API_KEY is not set, and skip W&B-auth tests when no WANDB_API_KEY or W&B .netrc credential resolves. The runFromTemplate smoke is reduced: it needs CWSANDBOX_TEMPLATE_ID and does not mint or delete that template. The default smoke suite uses default internet egress and network.denyEgress for the no-internet check. Hostname-grant smoke probes pypi.org and *.pypi.org over HTTPS and skips when the fleet cannot admit names. Stress smoke is intentionally not part of pnpm check; it creates live sandboxes and uses bounded workloads to exercise larger logs, streams, stdin, files, pagination, and cleanup paths.
License
This package is licensed under the Apache-2.0 license. See
LICENSE-Apache-2.0.txt and NOTICE.
Repository examples under examples/ are licensed under the BSD-3-Clause license.