| name | upstash-box-js |
| description | Work with the @upstash/box TypeScript/JavaScript SDK for sandboxed cloud containers with AI agents, shell, filesystem, git, cron schedules, and a headless browser. Use when building with Upstash Box, creating sandboxed environments, running AI agents in containers, browser automation from a box, or orchestrating parallel boxes. |
@upstash/box SDK
Sandboxed cloud containers with built-in AI agents, shell, filesystem, git, cron schedules, and an optional headless browser.
The Python SDK (upstash-box) mirrors this API with snake_case names — see the
upstash-box-py skill for the Python spelling of everything below.
Install & Setup
npm install @upstash/box
npm install zod
Set UPSTASH_BOX_API_KEY env var or pass apiKey to constructors.
Anonymous telemetry headers are sent by default. Opt out with the
UPSTASH_DISABLE_TELEMETRY env var, or enableTelemetry: false in the config
(the only option on runtimes without process.env, e.g. Cloudflare Workers).
Box Lifecycle
import { Box, Agent, ClaudeCode, BoxApiKey } from "@upstash/box"
const box = await Box.create({
name: "my-box",
runtime: "node",
size: "small",
labels: ["beta", "x-team"],
keepAlive: true,
initCommand: "npm install && npm run dev",
browser: true,
agent: {
harness: Agent.ClaudeCode,
model: ClaudeCode.Sonnet_4_5,
apiKey: BoxApiKey.UpstashKey,
},
git: {
token: process.env.GITHUB_TOKEN,
userName: "Bot",
userEmail: "bot@example.com",
},
env: { DATABASE_URL: "..." },
skills: ["upstash/qstash-js/qstash-js"],
timeout: 600_000,
debug: false,
})
const same = await Box.get(box.id, { gitToken: process.env.GITHUB_TOKEN })
const byName = await Box.getByName("my-box")
const all = await Box.list()
const beta = await Box.list({ label: "beta" })
await box.pause()
await box.resume()
await box.delete()
const { status } = await box.getStatus()
box.id; box.size; box.keepAlive; box.cwd; box.networkPolicy
await box.setInitCommand("npm run dev")
const script = await box.getInitCommand()
await box.deleteInitCommand()
await Box.delete({ boxIds: ["box_1", "box_2"] })
const { deleted } = await Box.deleteSnapshots({ snapshotIds: ["snap_1"] })
Account-level env vars
Injected into every box you create.
await Box.setEnv("API_TOKEN", "secret")
const env = await Box.listEnv()
await Box.setAllEnv({ A: "1", B: "2" })
await Box.deleteEnv("API_TOKEN")
Agent Runs
import { z } from "zod"
const run = await box.agent.run({
prompt: "Review the code for security issues",
responseSchema: z.object({
verdict: z.enum(["approved", "changes_requested"]),
findings: z.array(z.object({
severity: z.enum(["high", "medium", "low"]),
file: z.string(),
issue: z.string(),
})),
}),
timeout: 120_000,
maxRetries: 2,
options: { maxTurns: 20, maxBudgetUsd: 1.0, effort: "high" },
onToolUse: (tool) => console.log(tool.name, tool.input),
onToolResult: (result) => console.log(result.toolCallId, result.),
})
run.
run.
run.
box..({ : , : [] })
box..({
: ,
: [{ : base64, : , : }],
})
stream = box..({ : })
( chunk stream) {
(chunk. === ) process..(chunk.)
(chunk. === ) process..(chunk.)
(chunk. === ) .(chunk., chunk.)
(chunk. === ) .(chunk.)
(chunk. === ) .(chunk., chunk., chunk.)
}
stream.
stream.
box..({
: ,
: { : , : { : } },
})
Agent options (per harness)
options is forwarded to the harness — the accepted keys depend on which one
the box runs. Typing the box (Box.create<Agent.ClaudeCode>({...})) narrows
options to that harness's shape.
{
maxTurns: 20,
maxBudgetUsd: 1.0,
effort: "high",
thinking: { type: "adaptive" },
disallowedTools: ["Bash"],
agents: { reviewer: { } },
promptSuggestions: false,
fallbackModel: "anthropic/claude-sonnet-4-5",
systemPrompt: "You are a release engineer.",
}
{
modelReasoningEffort: "high",
modelReasoningSummary: "concise",
personality: "pragmatic",
webSearch: "live",
}
{
reasoningEffort: "high",
textVerbosity: "low",
reasoningSummary: "auto",
: { : , : },
}
Codex keys are converted to the backend's snake_case for you — always write them camelCase.
Harness & model
harness is required (provider / runner are deprecated aliases). Model
enums: ClaudeCode, OpenAICodex, OpenCodeModel, CursorModel,
OpenRouterModel, VercelModel — or any plain provider-prefixed string.
import { ClaudeCode, OpenAICodex, OpenCodeModel, CursorModel, OpenRouterModel, VercelModel } from "@upstash/box"
ClaudeCode.Opus_5
ClaudeCode.Sonnet_5
OpenAICodex.GPT_5_6
OpenCodeModel.Claude_Opus_5
CursorModel.Composer_2_5
OpenRouterModel.Claude_Opus_5
VercelModel.GPT_5_5
const { harness, model } = box.modelConfig
await box.configureModel("anthropic/claude-opus-4-8")
import { inferDefaultProvider } from "@upstash/box"
inferDefaultProvider("openai/gpt-5.6")
inferDefaultProvider("cursor/default")
Custom harness
Run your own agent binary inside the box instead of a managed harness.
import { Box, Agent, runCustomHarness } from "@upstash/box"
const box = await Box.create({
agent: {
harness: Agent.Custom,
model: "my-agent",
customHarness: { command: "node", args: ["/workspace/home/agent.js"], protocol: "box-sse-v1" },
},
})
await box.configureCustomHarness({ command: "node", args: ["/workspace/home/agent2.js"] })
await runCustomHarness(async ({ prompt, model, sessionId, stream, args }, emit) => {
emit.text("working...")
emit.reasoning("thinking out loud")
emit.tool({ toolCallId: "1", name: "Bash", input: { command: } })
emit.({ : , : })
emit.(, { : })
{
: ,
: ,
: ,
: ,
: ,
sessionId,
}
})
Run Fields
Every run (agent, command, or code) returns a Run<T>:
const run = await box.exec.command("npm test")
run.id
run.status
run.result
run.stdout
run.stderr
run.exitCode
run.cost
await run.cancel()
const logs = await run.logs()
const entries = await box.logs({ limit: 100, offset: 0 })
const runs = await box.listRuns()
Shell Execution
const run = await box.exec.command("echo hello && ls -la")
const run2 = await box.exec.code({ code: "console.log(1+1)", lang: "js", timeout: 10_000 })
const stream = await box.exec.stream("npm run build")
const stream2 = await box.exec.streamCode({ code: "print('hi')", lang: "python" })
for await (const chunk of stream) {
}
Filesystem
await box.files.write({ path: "/workspace/home/app.js", content: "console.log('hi')" })
const content = await box.files.read("/workspace/home/app.js")
const entries = await box.files.list("/workspace/home")
await box.files.write({ path: "/workspace/home/image.png", content: base64String, encoding: "base64" })
const b64 = await box.files.read("/workspace/home/image.png", { encoding: "base64" })
await box.files.upload([{ path: "./local/file.txt", destination: "/workspace/home/file.txt" }])
await box.files.download({ folder: "src" })
await box..()
cd / Working Directory
The SDK tracks cwd client-side. All operations (exec, files, git, agent) run relative to it.
box.cwd
await box.cd("my-repo")
await box.cd("/workspace/home/other")
Git
Clones land inside the box's isolated container, never on the caller's machine. Cloned
code is data until something runs it — treat an untrusted repo as untrusted input, and
pair it with a restrictive networkPolicy (see below) before running its build or tests.
await box.git.clone({ repo: "github.com/org/repo", branch: "main" })
await box.git.clone({ repo: "github.com/org/repo", depth: 1 })
await box.cd("repo")
const status = await box.git.status()
const diff = await box.git.diff()
const { sha } = await box.git.commit({
message: "fix: resolve bug",
authorName: "Jane Doe",
authorEmail: "jane@example.com",
})
await box.git.push({ branch: "feature/fix" })
await box.git.checkout({ branch: "release/v2" })
const pr = await box.git.createPR({ title: , : , : })
cfg = box..({ : , : })
{ output } = box..({ : [, , ] })
Schedules
Cron tasks on a box — shell commands or agent prompts. Available on Box and EphemeralBox. Cron is UTC.
const execSchedule = await box.schedule.exec({
cron: "* * * * *",
command: ["bash", "-c", "date >> /workspace/home/cron.log"],
folder: "/workspace/home",
webhookUrl: "https://example.com/hook",
webhookHeaders: { Authorization: "Bearer ..." },
})
const agentSchedule = await box.schedule.agent({
cron: "0 9 * * *",
prompt: "Run the test suite and fix any failures",
folder: "/workspace/home/repo",
model: "anthropic/claude-sonnet-5",
options: { maxBudgetUsd: 1.0, effort: "high" },
timeout: 300_000,
webhookUrl: "https://example.com/hook",
webhookHeaders: { Authorization: "Bearer ..." },
})
const schedules = await box.schedule.list()
const one = await box..(agentSchedule.)
box..(agentSchedule., { : , : })
box..(agentSchedule.)
box..(agentSchedule.)
box..(agentSchedule.)
Snapshots
const snap = await box.snapshot({ name: "after-setup" })
const restored = await Box.fromSnapshot(snap.id, {
size: "medium",
keepAlive: true,
git: { token: process.env.GITHUB_TOKEN, userName: "Bot", userEmail: "bot@example.com" },
env: { DATABASE_URL: "..." },
})
const snaps = await box.listSnapshots()
await box.deleteSnapshot(snap.id)
Browser
Create the box with browser: true to drive a headless Chromium. Tab management
lives on box.browser; every page operation lives on the Tab handle.
extract / observe / act(instruction) are AI-powered and metered;
act(action) replays an already-resolved action with no LLM call and no tokens.
import { z } from "zod"
const box = await Box.create({
browser: true,
agent: { harness: Agent.ClaudeCode, model: ClaudeCode.Sonnet_4_5 },
})
const tab = await box.browser.tab.create("https://example.com", { waitUntil: "load", timeout: 30_000 })
const tabs = await box.browser.listTabs()
const again = box.browser.getTab(tab.id)
tab.id; tab.url; tab.title
const content = await tab.goto("https://news.ycombinator.com")
const current = await tab.content()
const png = await tab.screenshot()
b64 = tab.({ : , : })
data = tab.(
,
z.({ : z.(), : z.() }),
{ : },
)
{ elements } = tab.(, { : })
acted = tab.()
tab.(elements[])
tab.(acted.[])
liveUrl = tab.()
cdpUrl = box..()
tab.()
{ chromium }
remote = chromium.(cdpUrl)
context = remote.()[] ?? ( remote.())
page = context.()[] ?? ( context.())
page.()
handle = box...({ : })
recording = handle.()
all = box...()
one = box...(recording.)
file = box...(recording.)
box...(recording., { : })
Multi-step browser goals
tab.run() — the autonomous multi-step browser agent — was removed in 0.7.0,
along with the BrowserRunOptions / BrowserRunResult / BrowserRunStep types
(Stagehand v4 dropped the underlying agent primitive). The DOM-aware browser now
exposes observe, act, and extract only. Three replacements:
1. Drive your own loop — resolve steps once with observe, then replay them
with act(action) so the model stays out of the hot path; extract is the stop check.
const { elements } = await tab.observe("the product links in the listing")
const actions = elements.filter((e) => e.selector)
for (const action of actions.slice(0, 5)) {
await tab.goto(START)
await tab.act(action)
const item = await tab.extract("title and price", z.object({ title: z.string() }))
}
2. Hand the goal to the in-box agent — browser: true auto-wires the
chrome-devtools MCP (Chromium already warmed on 127.0.0.1:9222) into the box's
coding agent, so box.agent.run({ prompt }) drives the browser itself and iterates
until done. No tab.create() needed first. This bills coding-agent model tokens
rather than browser-AI metering, and needs an agent harness + key.
3. Connect over CDP with Playwright / Puppeteer via box.browser.cdpUrl() when
the flow is fully deterministic.
EphemeralBox
Lightweight, short-lived boxes (max 3 days). Supports exec, files, schedule, cd, network policy, and snapshots. No agent, git, skills, labels namespace, browser, or public URLs.
import { EphemeralBox } from "@upstash/box"
const ebox = await EphemeralBox.create({
name: "scratch-box",
runtime: "python",
size: "small",
ttl: 3600,
env: { API_KEY: "..." },
labels: ["scratch"],
networkPolicy: { mode: "deny-all" },
attachHeaders: { "api.stripe.com": { Authorization: "Bearer sk_live_..." } },
})
ebox.networkPolicy
ebox.expiresAt
await ebox.exec.command("python -c 'print(1+1)'")
await ebox.exec.code({ code: "print('hi')", lang: "python" })
await ebox.files.write({ path: "/workspace/home/data.json", content: "{}" })
await ebox..({ : , : [, , ] })
ebox.()
snap = ebox.({ : })
ebox.()
ebox.(snap.)
{ status } = ebox.()
ebox.()
ebox2 = .(snap., { : })
Public URLs
Expose box ports as public URLs with optional auth.
const publicURL = await box.getPublicURL(3000)
const authed = await box.getPublicURL(3000, { bearerToken: true })
const basic = await box.getPublicURL(3000, { basicAuth: true })
const { publicURLs } = await box.listPublicURLs()
await box.deletePublicURL(3000)
Skills
Install agent skills from the Context7 registry. Format: owner/repo/skill-name.
An installed skill becomes instructions for the box's agent, so pin skills to owners you
trust the same way you would a dependency. Skills resolve from the registry at box
creation, not from arbitrary URLs, and they only ever run inside the box's container.
const box = await Box.create({ skills: ["upstash/qstash-js/qstash-js"] })
await box.skills.add("upstash/workflow-js/workflow-js")
const enabled = await box.skills.list()
await box.skills.remove("upstash/workflow-js/workflow-js")
Labels
const labels = await box.labels.add("prod")
await box.labels.remove("beta")
const current = await box.labels.list()
const prodBoxes = await Box.list({ label: "prod" })
Network Policy & Outbound Headers
const box = await Box.create({
networkPolicy: {
mode: "custom",
allowedDomains: ["api.example.com"],
allowedCidrs: ["203.0.113.0/24"],
deniedCidrs: ["10.0.0.0/8"],
},
attachHeaders: {
"api.stripe.com": { Authorization: "Bearer sk_live_..." },
"*.example.com": { "X-Custom-Token": "secret123" },
},
})
box.networkPolicy
await box.updateNetworkPolicy({ mode: "deny-all" })
MCP Servers
Attach MCP servers to the box agent. An attached server supplies tools the agent can call,
so use servers you control or trust — and keep networkPolicy restrictive when the agent
also handles untrusted input.
const box = await Box.create({
agent: { harness: Agent.ClaudeCode, model: ClaudeCode.Sonnet_4_5 },
mcpServers: [
{ name: "fs", package: "@modelcontextprotocol/server-filesystem", args: [] },
{ name: "custom", url: "<your-mcp-server-url>", headers: { Authorization: "..." } },
],
})
Errors & SSH
import { BoxError } from "@upstash/box"
try {
await box.agent.run({ prompt: "..." })
} catch (e) {
if (e instanceof BoxError) console.error(e.message, e.statusCode)
}
Shell into a box directly (Box API key is the SSH password):
ssh <box-id>@us-east-1.box.upstash.com
Gotchas
- Default working directory is
/workspace/home, not /home or /
box.cd() is client-side tracking — it validates the path exists but doesn't change the box's shell cwd. All SDK methods use it automatically.
agent.harness is required; provider / runner still work but are deprecated
- There is no
box.fork() — it was removed from the SDK. Snapshot the box and use Box.fromSnapshot() instead.
EphemeralBox does NOT support agent, git, skills, browser, or public URLs — use full Box for those (it does support schedule and snapshots)
run.exitCode is null for agent runs, only available for exec commands
run.result is stdout on success and stderr on failure — a command that exits 0 writing only to stderr yields ""; read run.stderr for it
files.download({ folder }) takes a path inside the box; output lands in ./<basename> locally
box.browser requires a box created with browser: true
- There is no
tab.run() — the autonomous browser agent was removed in 0.7.0. Loop observe + act(action) + extract yourself, hand the goal to the in-box agent, or drive Playwright over cdpUrl()
tab.act(action) (replaying an observe() result) costs no tokens and needs no model provider key; only act(instruction) with a string is metered
getInitCommand / setInitCommand / deleteInitCommand throw unless the box was created with keepAlive: true
box.delete() is irreversible — snapshot first if you need the state
- Git operations require
git.token in BoxConfig for private repos and PRs
- creates a new box — it does not modify the original, and it does not forward , , or from the config you pass