@coreweave/cwsandbox
v0.5.0-beta.0
Published
TypeScript SDK for CoreWeave Sandbox.
Readme
CWSandbox JS
TypeScript SDK for CoreWeave Sandbox.
Beta: public API may still change. 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, andshowTerminated. Profiles,ports, andincludeStoppedare not part of this API. UserunFromTemplate/withSandboxFromTemplatefor organization templates.
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@betaOther package managers:
pnpm add @coreweave/cwsandbox@beta
yarn add @coreweave/cwsandbox@betaUse an API key with the Node gRPC client:
export CWSANDBOX_API_KEY="..."
export CWSANDBOX_BASE_URL="https://api.cwsandbox.com" # Optional.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" # Optional.
export WANDB_PROJECT="sandbox" # Optional.
export WANDB_SANDBOX_BASE_URL="..." # Optional gateway override.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-tanstackadapts this SDK to TanStack AI'sSandboxProvidercontract fordefineSandbox(...)/withSandbox(...)workflows (same lockstep version; private until its fast-follow publish).@coreweave/cwsandbox-computesdkadapts this SDK to ComputeSDK'sdefineProvidercontract via thecoreweave(...)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-startTypecheck the SDK recipe package without creating a live sandbox:
pnpm --dir examples/sdk typecheckIntegration examples:
pnpm --dir examples/weave start
pnpm --dir examples/tanstack start
pnpm --dir examples/weave typecheck
pnpm --dir examples/tanstack typecheckAPI Map
- Construct clients only through the Node/W&B factories (
createSandboxClient,createSandboxClientFromEnv). Direct construction and transport replacement are not supported public APIs in this beta. SandboxClientcreates, reconnects, lists, and deletes sandboxes.createSandboxClientFromEnv()in@coreweave/cwsandbox/nodewires 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/writeStreamfor incremental transfers).sandbox.logs.*reads or streams the sandbox main process logs.sandbox.wait(...),sandbox.stop(...), andsandbox.delete(...)manage lifecycle.stop()requests shutdown and waits until the sandbox is terminal; usewait({ 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, 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 {
// Resolves after Stop RPC and the sandbox reaches completed/failed/terminated.
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.
To watch an already-stopping sandbox without sending Stop:
await sandbox.wait({ targetStatus: "terminal", timeoutMs: 60_000 });
console.log(sandbox.status); // completed | failed | terminated
console.log(sandbox.exitCode); // PID-1 exit code when the backend observed itwait() 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.withSandbox(...),
and client.withSandboxFromTemplate(...) 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(); // Sends EOF to the process.
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
| API | Shape | Best for |
| ---------------------------------------- | ------------------------------------ | ------------------------------------------------------- |
| 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.
// Incremental write: bare buffer is sliced into 64 KiB chunks, or pass an iterable.
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]),
]);
// Incremental read: drain promptly into a fast local sink (avoid slow work here).
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
signalabort onwriteStreammay leave a partial remote file. - Early stop / abort on
readStreambest-effort cancels the StreamExec process. readStream({ timeoutMs })is one wall-clock across the integritystatand the file transfer. The clock starts when iteration begins. OmittimeoutMsfor an unbounded transfer (statis 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) throwCWSandboxValidationError.
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",
},
],
});storemust match a Gateway-registered secret store name for the organization. For W&B team secrets this is typicallywandb-team-secrets.nameis the secret id in that store (protopath).fieldis optional for structured secrets.envVardefaults tonamewhen 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" } 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);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, // PID-1 / primary-container code, not a command result
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);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 forcwsandbox.commetadata— ErrorInfo metadata map (always an object; empty when absent)retryDelayMs— optional RetryInfo hint
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
# CWSANDBOX_TEMPLATE_ID= # optional reduced runFromTemplate smoke
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 checkUseful root commands:
pnpm testruns core unit tests.pnpm test:typesruns public API type tests.pnpm test:readmetypechecks TypeScript examples in this README.pnpm test:packagebuilds the package and checks real package exports from fixture consumers.pnpm format:fixapplies Oxfmt formatting.pnpm lint:fixapplies Oxlint fixes.pnpm fixruns lint fixes and formatting.pnpm smokeruns the credential-gated live e2e smoke suite, including W&B auth whenWANDB_API_KEYor a W&B.netrcentry is available.pnpm smoke:stressruns the credential-gated standard stress smoke suite.pnpm smoke:stress -- --heavyruns 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.
