@upstash/box
v0.7.2
Published
Upstash Box SDK - TypeScript client for async and parallel AI coding agents
Readme
@upstash/box
TypeScript SDK for Upstash Box — create sandboxed AI coding agents with streaming, structured output, file I/O, git operations, and snapshots.
Installation
npm install @upstash/boxQuick start
import { Box, Agent } from "@upstash/box";
const box = await Box.create({
runtime: "node",
agent: { harness: Agent.ClaudeCode, model: "anthropic/claude-sonnet-5" },
});
const run = await box.agent.run({
prompt: "Create a hello world Express server",
});
console.log(run.result);
await box.delete();Authentication
Pass apiKey in the config or set the UPSTASH_BOX_API_KEY environment variable.
API
Static methods
Box.create(config: BoxConfig): Promise<Box>
Create a new sandboxed box.
import { Box, Agent, BoxApiKey } from "@upstash/box";
const box = await Box.create({
apiKey: "box_...", // or set UPSTASH_BOX_API_KEY
runtime: "node", // "node" | "python" | "golang" | "ruby" | "rust"
labels: ["beta", "x-team"], // tag the box for organization/filtering
keepAlive: true,
initCommand: "npm install && npm run dev",
agent: {
harness: Agent.ClaudeCode,
model: "anthropic/claude-sonnet-5",
apiKey: BoxApiKey.UpstashKey, // Upstash-managed key
// apiKey: BoxApiKey.StoredKey, // use a key stored via the Upstash console
// apiKey: process.env.CLAUDE_KEY!, // or pass a direct API key
},
// Optional git identity configured in the container on create
// Defaults: "Upstash Box" / "[email protected]"
git: {
token: process.env.GITHUB_TOKEN!,
userName: "John Doe",
userEmail: "[email protected]",
},
env: { NODE_ENV: "production" },
timeout: 600000,
debug: false,
});Box.get(boxId: string, options?: BoxGetOptions): Promise<Box>
Reconnect to an existing box by ID.
const box = await Box.get("box_abc123");Box.getByName(name: string, options?: BoxGetOptions): Promise<Box>
Get an existing box by name.
const box = await Box.getByName("my-box");Box.list(options?: ListOptions): Promise<BoxData[]>
List all boxes for the authenticated user. Pass label to return only boxes carrying that label.
const boxes = await Box.list();
const betaBoxes = await Box.list({ label: "beta" });Box.fromSnapshot(snapshotId: string, config: BoxConfig): Promise<Box>
Create a new box from a saved snapshot.
const box = await Box.fromSnapshot("snap_abc123", {
agent: { harness: Agent.ClaudeCode, model: "anthropic/claude-sonnet-5" },
keepAlive: true,
initCommand: "npm install && npm run dev",
});SSH
You can also connect directly to a box shell:
ssh <box-id>@us-east-1.box.upstash.comUse your Box API key as the SSH password.
Agent
box.agent.run(options: RunOptions): Promise<Run>
Run the AI agent with a prompt. Supports streaming, structured output with Zod schemas, timeouts, retries, tool use callbacks, and webhooks.
// Structured output
import { z } from "zod";
const schema = z.object({
name: z.string(),
score: z.number(),
});
const run = await box.agent.run({
prompt: "Analyze this candidate",
responseSchema: schema,
});
const result = run.result; // typed as { name: string, score: number }
const stream = await box.agent.stream({
prompt: "Refactor the auth flow",
});
for await (const part of stream) {
if (part.type === "text-delta") process.stdout.write(part.text);
if (part.type === "tool-call") console.log(part.toolName, part.input);
if (part.type === "finish") console.log(part.usage.inputTokens + part.usage.outputTokens);
}box.exec.command(command: string): Promise<Run>
Execute a shell command in the box.
const run = await box.exec.command("node index.js");
console.log(run.result);box.exec.session(options): Promise<ExecSessionHandle>
Start a live command session. exec.command resolves once the command has
finished; session resolves as soon as it starts, so you can write to stdin,
resize a PTY, and signal the process while it runs.
This one is Node-only. Authentication travels in a request header, and browsers cannot set headers on a WebSocket handshake.
let out = "";
const session = await box.exec.session({
argv: ["sort"], // exact program + args, no shell
onStdout: (b) => (out += Buffer.from(b).toString()),
});
session.write("banana\napple\n");
session.endStdin(); // EOF, so sort finishes
await session.wait(); // 0Pass cmd instead of argv to run through bash -lc, tty: true (with
rows / cols) to allocate a PTY sized correctly from the first read, and
cwd / env to place the process. env entries are KEY=VALUE strings
overlaid on the box environment.
The handle exposes pid and execId, plus write, endStdin, resize,
kill(signal), terminate(graceMs), wait, and close. It owns the process:
close(), a dropped connection, or exiting your program all terminate the
command rather than leaving it running in the box, and sessions cannot be
reattached.
const dev = await box.exec.session({ cmd: "npm run dev", tty: true, rows: 24, cols: 80 });
dev.write("rs\n"); // restart
dev.resize(50, 120);
dev.terminate(2000); // SIGTERM, then SIGKILL after 2sFiles
await box.files.write({ path: "hello.txt", content: "Hello!" });
const content = await box.files.read("hello.txt");
const entries = await box.files.list(".");
await box.files.upload([{ path: "./local.txt", destination: "remote.txt" }]);
await box.files.download({ folder: "output/" });Git
await box.git.clone({ repo: "https://github.com/user/repo", branch: "main" });
await box.git.clone({ repo: "https://github.com/user/repo", depth: 1 }); // shallow clone
const diff = await box.git.diff();
const status = await box.git.status();
await box.git.commit({
message: "feat: add feature",
authorName: "Jane Doe",
authorEmail: "[email protected]",
});
const gitConfig = await box.git.updateConfig({
userName: "John Doe",
userEmail: "[email protected]",
});
console.log(gitConfig.git_user_name, gitConfig.git_user_email);
await box.git.push({ branch: "main" });
const pr = await box.git.createPR({ title: "New feature", body: "Description" });
// Run an arbitrary git command
const result = await box.git.exec({ args: ["log", "--oneline", "-5"] });
console.log(result.output);
// Switch branches
await box.git.checkout({ branch: "feature-branch" });Schedules
Create recurring tasks that run on a cron schedule — either shell commands (exec) or AI agent prompts (agent). Schedules can be paused, resumed, and deleted. Available on both Box and EphemeralBox.
// Schedule a shell command to run every minute
const execSchedule = await box.schedule.exec({
cron: "* * * * *",
command: ["bash", "-c", "date >> /workspace/home/cron.log && echo scheduled-ok"],
});
// Schedule an agent prompt to run daily at 9am
const agentSchedule = await box.schedule.agent({
cron: "0 9 * * *",
prompt: "Run the test suite and fix any failures",
timeout: 300_000, // 5 minute timeout per run
options: { maxBudgetUsd: 1.0, effort: "high" }, // agent options (provider-specific)
});
// List all active and paused schedules.
const schedules = await box.schedule.list();
// Get a specific schedule by ID.
const schedule = await box.schedule.get("sched-abc123");
// Update a schedule (partial — omitted fields keep their current value).
// Empty values ("" / [] / {}) clear a field; `options: null` clears agent
// options. The schedule's type (exec vs agent) cannot be changed.
const updated = await box.schedule.update(schedule.id, {
cron: "0 18 * * *",
webhookUrl: "", // clears the webhook
});
// Pause a schedule. It won't fire until resumed.
await box.schedule.pause(schedule.id);
// Resume a paused schedule.
await box.schedule.resume(schedule.id);
// Delete a schedule permanently.
await box.schedule.delete(schedule.id);Labels
Tag a box for organization and filtering. Labels are set at create time (labels) and managed on a running box via the labels namespace. Each add/remove returns the box's updated label set. Filter with Box.list({ label }).
// Add / remove — returns the updated label set
const labels = await box.labels.add("prod"); // ["beta", "x-team", "prod"]
await box.labels.remove("beta"); // ["x-team", "prod"]
// List this box's labels
const current = await box.labels.list();Working directory
box.cwd; // "/workspace/home" (default)
await box.cd("my-project");
box.cwd; // "/workspace/home/my-project"
// All operations now run relative to my-project/
const run = await box.exec.command("ls");
const files = await box.files.list();
const status = await box.git.status();
await box.cd(".."); // back to /workspace/homeModel configuration
// Read the current provider and model
const { harness, model } = box.modelConfig;
// Change the model
await box.configureModel("anthropic/claude-opus-4-8");
// modelConfig reflects the change immediately
box.modelConfig.model; // "anthropic/claude-opus-4-8"Lifecycle
await box.pause(); // Pause (preserves state)
await box.resume(); // Resume
await box.delete(); // Permanent delete
const { status } = await box.getStatus();Keep-alive boxes also support init-command management:
const script = await box.getInitCommand();
await box.setInitCommand("npm run dev");
await box.deleteInitCommand();
box.keepAlive; // booleanSnapshots
const snapshot = await box.snapshot({ name: "checkpoint-1" });
const snapshots = await box.listSnapshots();
await box.deleteSnapshot(snapshot.id);Run object
Every agent.run() and exec.command() call returns a Run object. Streaming methods (agent.stream(), exec.stream()) return a StreamRun which extends Run and is async-iterable.
const run = await box.agent.run({ prompt: "..." });
run.id; // Run ID
run.result; // Final output (typed if schema provided)
run.status; // "running" | "completed" | "failed" | "cancelled" | "detached"
run.exitCode; // Process exit code (command/code runs)
run.stdout; // Raw stdout (command/code runs)
run.stderr; // Raw stderr (command/code runs)
run.cost; // { inputTokens, outputTokens, computeMs, totalUsd }
await run.cancel(); // Abort
await run.logs(); // Filtered log entries
// Streaming returns a StreamRun — async-iterable with typed Chunk objects
const stream = await box.agent.stream({ prompt: "..." });
for await (const chunk of stream) {
if (chunk.type === "text-delta") process.stdout.write(chunk.text);
}
stream.status; // "completed" after iteration finishes
stream.result; // final outputAgents
The preferred field in agent config is harness, and it is required. Deprecated aliases provider and runner still work for backward compatibility.
| Enum | Value |
| ------------------ | ------------- |
| Agent.ClaudeCode | claude-code |
| Agent.Codex | codex |
| Agent.OpenCode | opencode |
Models
Claude Code
| Enum | Value |
| ----------------------- | ----------------------------- |
| ClaudeCode.Fable_5 | anthropic/claude-fable-5 |
| ClaudeCode.Opus_5 | anthropic/claude-opus-5 |
| ClaudeCode.Opus_4_8 | anthropic/claude-opus-4-8 |
| ClaudeCode.Opus_4_7 | anthropic/claude-opus-4-7 |
| ClaudeCode.Opus_4_5 | anthropic/claude-opus-4-5 |
| ClaudeCode.Opus_4_6 | anthropic/claude-opus-4-6 |
| ClaudeCode.Sonnet_4 | anthropic/claude-sonnet-4 |
| ClaudeCode.Sonnet_4_5 | anthropic/claude-sonnet-4-5 |
| ClaudeCode.Sonnet_4_6 | anthropic/claude-sonnet-4-6 |
| ClaudeCode.Sonnet_5 | anthropic/claude-sonnet-5 |
| ClaudeCode.Haiku_4_5 | anthropic/claude-haiku-4-5 |
OpenAI Codex
| Enum | Value |
| --------------------------------- | ------------------------------ |
| OpenAICodex.GPT_5_6 | openai/gpt-5.6 (alias → Sol) |
| OpenAICodex.GPT_5_6_Sol | openai/gpt-5.6-sol |
| OpenAICodex.GPT_5_6_Terra | openai/gpt-5.6-terra |
| OpenAICodex.GPT_5_6_Luna | openai/gpt-5.6-luna |
| OpenAICodex.GPT_5_5 | openai/gpt-5.5 |
| OpenAICodex.GPT_5_4 | openai/gpt-5.4 |
| OpenAICodex.GPT_5_4_Mini | openai/gpt-5.4-mini |
| OpenAICodex.GPT_5_3_Codex | openai/gpt-5.3-codex |
| OpenAICodex.GPT_5_3_Codex_Spark | openai/gpt-5.3-codex-spark |
| OpenAICodex.GPT_5_2_Codex | openai/gpt-5.2-codex |
| OpenAICodex.GPT_5_1_Codex_Max | openai/gpt-5.1-codex-max |
OpenRouter
| Enum | Value |
| ---------------------------------- | --------------------------------------- |
| OpenRouterModel.Claude_Fable_5 | openrouter/anthropic/claude-fable-5 |
| OpenRouterModel.Claude_Opus_5 | openrouter/anthropic/claude-opus-5 |
| OpenRouterModel.Claude_Sonnet_5 | openrouter/anthropic/claude-sonnet-5 |
| OpenRouterModel.Claude_Opus_4_5 | openrouter/anthropic/claude-opus-4-5 |
| OpenRouterModel.Claude_Sonnet_4 | openrouter/anthropic/claude-sonnet-4 |
| OpenRouterModel.Claude_Haiku_4_5 | openrouter/anthropic/claude-haiku-4-5 |
| OpenRouterModel.DeepSeek_R1 | openrouter/deepseek/deepseek-r1 |
| OpenRouterModel.Gemini_2_5_Pro | openrouter/google/gemini-2.5-pro |
| OpenRouterModel.Gemini_2_5_Flash | openrouter/google/gemini-2.5-flash |
| OpenRouterModel.GPT_5_6_Sol | openrouter/openai/gpt-5.6-sol |
| OpenRouterModel.GPT_5_6_Terra | openrouter/openai/gpt-5.6-terra |
| OpenRouterModel.GPT_5_6_Luna | openrouter/openai/gpt-5.6-luna |
| OpenRouterModel.GPT_4_1 | openrouter/openai/gpt-4.1 |
| OpenRouterModel.O3 | openrouter/openai/o3 |
| OpenRouterModel.O4_Mini | openrouter/openai/o4-mini |
Box Sizes
Boxes have configurable resource sizes, set at creation time via the size option. Defaults to "small".
| Size | CPU | Memory |
| -------- | ------- | ------ |
| small | 2 cores | 4 GB |
| medium | 4 cores | 8 GB |
| large | 8 cores | 16 GB |
const box = await Box.create({
size: "large",
agent: { harness: Agent.ClaudeCode, model: "anthropic/claude-sonnet-5" },
});
console.log(box.size); // "large"Also supported in Box.fromSnapshot():
const box = await Box.fromSnapshot("snap_abc123", { size: "medium" });Runtimes
Runtime is a string union type: "node" | "python" | "golang" | "ruby" | "rust", plus their Alpine variants: "node-alpine" | "python-alpine" | "golang-alpine" | "ruby-alpine" | "rust-alpine"
Examples
See the examples/ directory for complete working examples:
basic.ts— Create a box, run an agent, read outputstreaming.ts— Parallel boxes with structured output (Zod)file-upload.ts— Upload local files into the boxgit-pr.ts— Clone a repo, make changes, create a PRsnapshot-restore.ts— Save and restore workspace statewebhook.ts— Fire-and-forget with webhook callbacksmulti-runtime.ts— Run across different runtimesmcp-skills.ts— Attach MCP servers to a box
Telemetry
The SDK sends anonymous usage telemetry with every API request, following the
same convention as the other Upstash SDKs: three HTTP headers reporting the SDK
version (Upstash-Telemetry-Sdk), the JS runtime (Upstash-Telemetry-Runtime,
e.g. [email protected]), and the deployment platform (Upstash-Telemetry-Platform,
e.g. vercel). No user data, request payloads, or identifiers are ever
collected.
To opt out, set the UPSTASH_DISABLE_TELEMETRY environment variable to any
value, or pass enableTelemetry: false in the client config (e.g.
Box.create({ enableTelemetry: false })). On runtimes without process.env
(such as Cloudflare Workers) the config option is the only way to opt out; the
env var takes precedence where both are available.
License
MIT
