Upstash Redis
Upstash v1.2.1
Publisher description
From the marketplace listing
Manage your Upstash resources from ChatGPT through the hosted Upstash MCP server (OAuth on first use): create and inspect Redis databases and run commands on them, publish QStash messages and manage schedules, build Vector and Search indexes, and drive Box sandboxes and Blob storage. Credentials stay on the server and are returned only when you ask for them.
Language: English · Automatically detected from descriptions.
Files & skills
File archives
Skill instructions
upstash-blob-js10.4 KB
---
name: upstash-blob-js
description: Work with the @upstash/blob TypeScript/JavaScript SDK for S3-compatible object storage with direct browser uploads, presigned URLs, multipart, and signed reads. Use when storing files or blobs, uploading avatars, images, videos, attachments or user documents, letting a browser upload straight to storage without proxying bytes through a server, generating public or time-limited signed URLs, serving private files, streaming large files with pause and resume, setting cache headers on stored objects, or reaching an S3-compatible bucket from the AWS SDK.
license: MIT
metadata:
author: Upstash
homepage: https://upstash.com
---
# @upstash/blob SDK
S3-compatible object storage. `Bucket` runs on your server; `uploadHandler` plus React hooks upload from the browser straight to storage so the bytes never pass through your app.
## Install & Setup
```bash
npm install @upstash/blob
```
Create a bucket in the console and set `UPSTASH_BLOB_TOKEN`. The token is a bearer secret for the whole bucket — keep it server side, never in `NEXT_PUBLIC_` or any bundler-inlined variable.
A bucket is **public** (every object has a URL) or **private** (no URL; reads go through `signedReadUrl`). That is a console setting, not a client option — the SDK learns it from the backend.
```ts
import { Bucket } from "@upstash/blob"
export const bucket = Bucket.fromEnv() // reads UPSTASH_BLOB_TOKEN
Bucket.fromEnv({ cache: "immutable" }) // default variable, plus options
Bucket.fromEnv("MEDIA_TOKEN", { cache: "immutable" }) // another variable, plus options
new Bucket({ token: env.UPSTASH_BLOB_TOKEN }) // Workers: no process.env
```
## Writing
```ts
const blob = await bucket.put("reports/q3.pdf", pdf, { contentType: "application/pdf" })
blob.url // public URL, undefined on a private bucket
blob.versionedUrl // url + ?v=<etag>, changes whenever the bytes do
blob.etag // what ifUnchanged takes
```
Bodies: `Request`, `Blob`/`File`, `ArrayBuffer`, typed array, `string`, `ReadableStream`. A stream carries no length — pass `size` (exact, streams through) or `maxSize` (buffers up to the cap), or `put` throws `length_required`.
| Option | Default | What it does |
|--------|---------|--------------|
| `contentType` | the body's, else `application/octet-stream` | What the object is stored as |
| `contentTypes` | any | Allow list, e.g. `["image/*", "application/pdf"]` |
| `maxSize` | none | Refuse a bigger body with `too_large` |
| `cache` | bucket default | `Cache-Control` stored with the object |
| `metadata` | none | `x-amz-meta-*`; lowercase keys, printable ASCII values |
| `allowOverwrite` | `true` | `false` refuses if something is there (`already_exists`) |
| `ifUnchanged` | none | An etag; fails with `conflict` if it changed |
| `multipart` | `'16mb'` | Threshold for going up in parts; `true`/`false` force it |
Sizes are **decimal**: `'20mb'` is 20,000,000 bytes. `'5mib'` throws.
```ts
import { uniquePath } from "@upstash/blob"
uniquePath`${user.id}/${file.name}` // 'u7/holiday-pic-3xK9mBqR.png'
```
Use `uniquePath` for any value you don't control. Each `${}` becomes one slugged filename that can never add a directory, and the finished path gets a random suffix — so two uploads of `photo.png` never collide. The literal parts of the template are passed through as written, so keep `.` and `..` out of them yourself: a path with those segments is refused later, by the call that uses it, with a `TypeError` rather than a `BlobError`.
`bucket.copy(from, to, { contentType, cache, metadata })` and `bucket.move(from, to, options)` preserve source properties you omit. `bucket.updateJson(path, fn, { maxAttempts: 6 })` retries a read-modify-write on conflict with backoff.
## Reading
```ts
const res = await bucket.get("reports/q3.pdf") // record + body: ReadableStream
const info = await bucket.info("reports/q3.pdf") // same record, no bytes (HEAD)
await bucket.exists("avatars/u7.png") // boolean instead of a throw
const page = await bucket.list({ prefix: "avatars/", limit: 1000 })
```
`get`/`info` throw `not_found`. Nothing is buffered — wrap the stream to read it:
```ts
await new Response((await bucket.get("notes/1.md")).body).text()
```
`list` pages with `page.cursor` (set only while more remains) and carries no `contentType` or `metadata`. `prefix` is the only filter — **keep your own table as the index** and treat the bucket as storage, not a queryable store.
```ts
const { url, expiresAt } = await bucket.signedReadUrl("private/report.pdf", {
expiresIn: "2m",
downloadAs: "Report Q3.pdf", // save under this name instead of rendering inline
})
```
Cache the link until `expiresAt`, never a deadline you compute — a link cannot outlive the credential that signed it, so you may get less than you asked for. `await bucket.publicUrl(path)` returns the public URL, `undefined` on a private bucket.
## Deleting
```ts
await bucket.del("avatars/me.png") // one path
await bucket.del(["a.png", "b.png"]) // an array, batched by 1000
await bucket.del({ prefix: "tmp/" }) // everything under a prefix
```
Already-gone counts as success, so deletes are safe to retry. An array or prefix delete where objects survive throws `partial_delete` with them in `e.failed`. `del({ prefix: '' })` is refused unless you pass `all: true`.
## Browser uploads
The handler authorizes and records; the bytes go browser → storage, so platform request body caps don't apply. Supply your application's `getUser` and `db.files.upsert` implementations below.
```ts
// lib/uploads.ts
import "server-only"
import { BlobError, uniquePath, uploadHandler } from "@upstash/blob"
import { getUser } from "@/lib/auth"
import { db } from "@/lib/db"
export const uploads = uploadHandler({
constraints: { maxSize: "20mb", contentTypes: ["image/*", "application/pdf"] },
onBeforeUpload: async ({ request, file }) => {
const user = await getUser(request)
if (!user) throw new BlobError("unauthorized") // nothing is signed
return { path: uniquePath`${user.id}/${file.name}`, metadata: { owner: user.id } }
},
onUploadComplete: async ({ uploadId, path, url, metadata }) => {
if (!metadata.owner) throw new BlobError("unauthorized")
await db.files.upsert({ id: uploadId, owner: metadata.owner, path, url })
return { path } // becomes upload.blob.data
},
})
```
```ts
// app/api/upload/route.ts
import { uploads } from "@/lib/uploads"
export const { GET, POST } = uploads
```
```ts
// lib/upload-hooks.ts
"use client"
import { uploadHooks } from "@upstash/blob/react"
import type { uploads } from "./uploads"
export const { useUpload } = uploadHooks<typeof uploads>()
```
```tsx
"use client"
import { useUpload } from "@/lib/upload-hooks"
export function UploadForm() {
const { start, upload, accept } = useUpload()
return <>
<input type="file" accept={accept} onChange={(e) => start({ file: e.target.files?.[0] })} />
{upload?.pending && <progress value={upload.percent} max={100} />}
{upload?.status === "done" && <a href={upload.blob.url}>{upload.blob.data.path}</a>}
{upload?.status === "error" && <p>{upload.error.message}</p>}
</>
}
```
`GET` serves the route's constraints, so `accept` fills the file dialog and an oversized file is refused before any request leaves the browser. `uploadHooks<typeof uploads>()` types route names and completion data at compile time; `import type` keeps server code out of the bundle.
This example assumes a **public** bucket. On a private one `url` is `undefined`, so store `path` instead and hand the client a `bucket.signedReadUrl(path)` when it needs to read.
Two rules that bite:
- **`onUploadComplete` can run more than once.** The browser retries it, so upsert on `uploadId` rather than inserting.
- **A throw out of `onUploadComplete` deletes the object.** That is right for a refusal and wrong for a transient database error — catch your own storage errors.
Files over 16 MB go up in parts, which is what gives `pause()`, `resume()`, per-part retry, and resume-after-reload when the user picks the same file again. `percent` caps at 99 until `status` is `done`; drive UI off `upload.pending`.
Use `multipart: true` on the handler to make every upload multipart. Then a closed tab leaves incomplete parts rather than a stored object nobody recorded, and one cron cleans up:
```ts
await bucket.abortStaleMultipartUploads({ olderThan: "1d" })
```
For routes where the bytes must pass through your app, write an ordinary route calling `bucket.put` and drive it with `useServerUpload` from `@upstash/blob/react`.
## Caching
`cache` is written once, at upload, and stored with the object — changing it means writing the object again.
| Value | Stored |
|-------|--------|
| `'immutable'` | `public, max-age=31536000, immutable` |
| `'revalidate'` | `public, max-age=0, must-revalidate` |
| `'no-store'` | `no-store` |
| a duration (`'15m'`, `3600`) | `public, max-age=<seconds>` |
`immutable` needs a path that changes when the bytes do — either `uniquePath` per upload, or a stable path served through `versionedUrl`. On a private bucket `private` replaces `public`.
## Errors
```ts
import { BlobError } from "@upstash/blob"
if (BlobError.is(e) && e.code === "not_found") return null
```
Use `BlobError.is()`, never `instanceof` — an ESM and a CJS copy are different classes. Codes: `not_found`, `already_exists`, `conflict`, `content_type_not_allowed`, `invalid_input`, `too_large`, `empty_body`, `length_required`, `signature_mismatch`, `unauthorized`, `forbidden`, `rate_limited`, `not_ready`, `partial_delete`, `move_left_a_copy`, `invalid_content_type_pattern`, `mint_backoff`, `request_failed`.
A refusal keeps its code all the way to the browser, so hooks switch on `error.code` rather than status numbers. Bad option values (`'5mib'`, a missing token) throw a `TypeError` where they are written, not a `BlobError` per request.
## S3 clients
```ts
import { GetObjectCommand, S3Client } from "@aws-sdk/client-s3"
const config = bucket.s3()
const s3 = new S3Client(config) // endpoint and credentials are async providers
// config.bucket is the underlying bucket id, available nowhere else
await s3.send(new GetObjectCommand({ Bucket: config.bucket, Key: "reports/q3.pdf" }))
```
Buckets are S3-compatible. Use this for what the SDK doesn't wrap — byte ranges, conditional GETs, tagging. Pass the providers through as they come so the AWS SDK can refresh an expired credential.
upstash-box-cli21.5 KB
---
name: upstash-box-cli
description: Drive an Upstash Box (a remote sandboxed workspace) from the terminal with the `box` CLI. Use when asked to run commands, edit files, clone repos, run builds or tests, publish a public URL, browse or screenshot a page, open a pull request or issue with a screenshot attached, schedule recurring work, run an AI agent, or do any work inside a box rather than on this machine.
---
`box` operates on a **remote container**, not this machine. Your own file and shell
tools act locally; anything that must happen inside the box goes through `box`.
## Install
```bash
npm i -g @upstash/box-cli
```
## Authentication
Every command needs an API key, or it fails with "API token required". Set it once,
or pass `--token` on any single command. Create one at
https://console.upstash.com/box.
```bash
export UPSTASH_BOX_API_KEY=box_...
```
## Selecting a box
Resolution order is `--box <id>`, then `$BOX_ID`, then the nearest `.box` file
(searched upward). Create one and pin it to the working directory:
```bash
box create --no-repl --runtime node # prints the id, writes .box
box create --no-repl --runtime node --clone-repo https://github.com/org/repo
box list # find an existing box
box use <box-id> # pin one to this directory
box status # id, where it came from, state
```
`--keep-alive`, `--browser`, `--env` and `--size` can only be chosen at create
time; to change any of them you make a new box. There is no resize.
Default to a plain `box create --no-repl`. A plain box pauses when it goes idle
and resumes on the next command, which is what almost all work wants:
```bash
box create --no-repl --browser # provision a headless Chromium
box create --no-repl --env KEY=VAL # env for this box (repeatable)
box create --no-repl --size medium # small (default), medium, large
```
Add `--keep-alive` only when something has to survive an idle gap: a detached
server you are about to reach over a preview URL, or a job that keeps running
between commands. It stops the box pausing, so the box keeps costing money until
you pause or delete it. `--init-command` is rejected without it:
```bash
box create --no-repl --keep-alive # stays up when idle
box create --no-repl --keep-alive --init-command "npm ci" # startup script
```
The rest of the create-time options, all equally unchangeable afterwards:
```bash
box create --no-repl --skill upstash/skills/redis # repeatable
box create --no-repl --mcp docs=@org/mcp-server # or name=https://url
box create --no-repl --mcp-file servers.json # for args and headers
box create --no-repl --network-policy deny-all # or allow-all, or custom
box create --no-repl --network-policy custom --allow-domain api.example.com
box create --no-repl --attach-headers-file headers.json
```
`--attach-headers-file` holds a JSON object keyed by host pattern
(`{"api.stripe.com": {"Authorization": "Bearer ..."}}`), and those headers are
injected into matching outbound requests from the box. There is an
`--attach-header host:Name=value` form too, but the value lands in `ps` and in
shell history, so prefer the file for anything secret.
A skill id has three parts, `owner/repo/skill-name`. A malformed one is only
warned about server-side, so the box comes up with the skill silently absent.
`--env` is per-box. `box env set` is account-level: it is merged into every box
created afterwards, never into one that already exists. A per-box `--env` wins
for the same key, so account-level values only fill in what the box did not set.
Account-level skills and MCP servers are merged the same way.
`paused` is not an error; the next command resumes the box.
A `.box` file is found by walking **up** from the working directory, so `cd`-ing
into another project can silently pick up a pin left there earlier and run
against the wrong box. In any session touching more than one box, pass `--box`
explicitly; `box status` says which box it resolved and where that came from.
Clean up when the work is done. Boxes cost money while they exist:
```bash
box pause # keeps the workspace, resumes on the next command
box resume # rarely needed; any command resumes a paused box
box delete --yes # irreversible; --yes is required without a terminal
```
Never run `box create` or `box connect` without `--no-repl`: they open an
interactive REPL and will hang. `box from-snapshot` takes `--no-repl` too.
```bash
box snapshot # snapshot this box, prints the id
box snapshot list
box from-snapshot <snapshot-id> --no-repl # restore into a new box, pinned
box snapshot delete <snapshot-id>
```
## Running commands
Put the remote command after `--`, or its flags are parsed as `box`'s own.
```bash
box exec -- npm install
box exec -C repo -- npm test
box exec --json -- node -e 'console.log(1)' # {stdout, stderr, exit_code}
```
The remote shell is `sh`, not bash. A heredoc inside `box exec` fails with
`Syntax error: redirection unexpected`; write the file with `box files write - `
instead, or wrap the command in `bash -c` when the box has bash.
For an interactive shell, ssh straight in. The box id is the user and the Box
API key is the password:
```bash
ssh <box-id>@us-east-1.box.upstash.com
```
Commands run as `boxuser`, so a global npm install needs sudo, which is
passwordless:
```bash
box exec -- 'sudo npm install -g @upstash/docs7' # EACCES without sudo
```
One argument is a shell expression, sent as written, so pipes and redirection work.
Several arguments are argv and are quoted individually, so an argument containing
spaces stays one argument.
The remote command's exit code is passed through, so `box exec -- npm test && ...`
chains normally. Exit code **125** means the CLI itself failed (bad box, bad flags),
never a status the remote command returned.
A background server dies with the command that started it. Detach it:
```bash
box exec -- '( npm run dev > dev.log 2>&1 & )'
box public-url 3000 # prints the public URL
box public-url list
box public-url delete 3000
```
Inline code, when a shell one-liner would be worse than a program:
```bash
box code - --lang python < script.py
box code 'console.log(1 + 1)' --lang js
```
## Building something and handing back a link
"Make me a snake game, use Upstash Box" is a request to build it in a box, run it
there, and reply with a URL the user can open. Do the whole thing; do not stop at
writing the file.
```bash
box create --no-repl --runtime node --keep-alive # writes .box; stays up for the URL
box files write index.html - <<'HTML'
<!doctype html><meta charset="utf-8"><title>Snake</title>
<canvas id="c" width="400" height="400"></canvas>
<script>/* the game */</script>
HTML
box files write server.js - <<'JS'
const http = require("http"), fs = require("fs");
http.createServer((_, res) => {
res.writeHead(200, { "Content-Type": "text/html" });
res.end(fs.readFileSync("index.html"));
}).listen(3000, "0.0.0.0"); // the default binds ::, which the check below misses
JS
box exec -- '( node server.js > server.log 2>&1 & )' # detached, or it dies
box exec -- 'sleep 1; ss -ltn | grep -q "0.0.0.0:3000" && echo up'
box public-url 3000 # the link to reply with
```
Node's own `http` module rather than a package: no install, no network fetch, and it
works on a bare `node` runtime.
Check the port before publishing it, and check what it is **bound to**, not just
that it answers. A server on `127.0.0.1` replies to a curl from inside the box
and still cannot be published: the proxy reaches the container by address, so
`box public-url` returns 502. That is why the check above greps for `0.0.0.0`
rather than curling localhost, which passes in exactly the case that fails.
Most dev servers need telling: `--host 0.0.0.0` for Vite and many others,
`-H 0.0.0.0` for some, and a few cannot be moved off loopback at all.
`--keep-alive` is what keeps the link working. Without it the box pauses when
idle, the detached server dies with it, and the URL you handed over starts
answering errors some minutes later.
Reply with the URL itself, not just "it is running". Say that the box keeps costing
money until `box delete --yes`, and that the URL is public to anyone who has it —
`box public-url 3000 --basic-auth` puts credentials in front of it.
## Files
Paths are relative to `/workspace/home`.
```bash
box files list src
box files read src/index.ts
box files write src/app.ts - < local.ts # - reads stdin: use this for code
box files write notes.txt "short text"
box files stat src/index.ts
box files mkdir -p a/b/c
box files rename old.ts new.ts
box files remove build -r # a directory needs -r
box files upload ./local.zip /workspace/home/local.zip
box files download repo # a folder lands in ./repo
box files download logs/app.log -o ./app.log # a file; -o names the destination
```
Write code with `-` and stdin. Passing source as an argument mangles it in the shell.
To search, use the box's own tools: `box exec -- grep -rn TODO src`.
## Git
A clone lands in a directory named after the repo, and every git verb except `clone`
needs that directory via `-C`. Without it git runs at the workspace root, which is not
a repository.
```bash
box git clone https://github.com/org/repo
box git clone https://github.com/org/repo -C my-app # -C is the destination here
box git status -C repo
box git diff -C repo
box git config -C repo --name "Bot" --email bot@example.com
box git checkout -C repo feature/x # creates the branch if missing
box git exec -C repo -- add -A
box git commit -C repo -m "message"
box git push -C repo # pushes the checked-out branch
box git create-pr -C repo --title "Fix the thing" --base main
box git create-pr -C repo --title "Fix the thing" --body-file notes.md
box git create-issue -C repo --title "Search returns nothing"
```
Use `--body-file` for anything longer than a sentence: a body worth writing does
not survive shell quoting. `-` reads stdin.
`box git exec` takes git's arguments without the leading `git`, and passes git's exit
code through.
Private repos and PRs need a token at creation: `box create --no-repl --git-token $GITHUB_TOKEN`.
## Attaching a screenshot to a pull request or issue
`--attach` uploads an image or video to the new pull request or issue, and repeats
for several. Alt text for an image goes after a `#`. A video renders as a player and
takes no alt text.
The path is read inside the box, relative to `-C`. A browser screenshot is written to
the machine running the CLI, not into the box, so it has to be uploaded first. That
upload is the step people miss:
```bash
box browser screenshot -o /tmp/shot.png # lands here, not in the box
box files upload /tmp/shot.png repo/shot.png # now it is in the repository
box git create-issue -C repo \
--title "Search returns nothing" \
--body 'Reproduced on staging.
' \
--attach 'shot.png#the empty result list'
```
A `` reference in the body is rewritten to point at the uploaded
asset, so the image renders in the issue instead of pointing at a path that exists
only inside the box.
Four rules are enforced, each a 400 before anything is created: the extension must be
png, jpg, jpeg, gif, webp, mp4, mov or webm; at most 50 files; the path must stay
inside the `-C` directory; and a video cannot carry alt text.
When some attachments upload and others fail, the item is still created and its URL
is still returned, with a `warning` alongside it. Text output prints the warning on
its own line, and `--json` carries it as the `warning` field. Check it before
reporting the issue as filed with its evidence attached.
## Agent
If the box was created with an agent, hand it a task:
```bash
box create --no-repl --agent-harness claude-code --agent-model anthropic/claude-sonnet-5
box run "Fix the failing test in src/auth.test.ts"
box run - < prompt.txt
```
Text goes to stdout, tool calls to stderr. Prefer doing the work yourself with the
commands above; `box run` is for delegating a whole task to the box's own agent.
## Watching and stopping work
```bash
box status runs # id, type, status, duration, cost
box status logs --limit 50
box cancel <run-id> # ids come from status runs
```
A run started by another process cannot be stopped any other way: `box cancel`
takes the id, so a long agent run or build is interruptible from a fresh shell.
## Browser
Only on a box created with `--browser`. Chromium **runs inside the box**, so it
reaches your app on `http://localhost:3000` with no public URL involved. What
lives outside is only the control path: these commands reach Chromium through
the API, so there is no `box exec` spelling of them. A script running in the box
can still talk to Chromium directly over CDP, which is the escape hatch at the
end of this section.
```bash
box browser open https://example.com # prints the tab id
box browser tabs
box browser content # title, url, text, links
box browser screenshot -o page.png
box browser goto https://example.com/login
box browser act "click the login button"
box browser close
box browser cdp-url # drive it with Playwright instead
box browser observe "what can I click here?"
box browser live-url # a URL for a human to watch the tab
```
Every `box browser act` is metered: it takes an instruction in words and needs
a model to read the page. The SDK can replay an `observe()` result for free,
but the CLI takes only the string form, so a loop of `act` calls costs a model
call each time. `content`, `goto`, `screenshot` and `close` are not metered.
Recordings, when you need to show what happened rather than describe it:
```bash
box browser recordings start --max-seconds 120
box browser recordings stop
box browser recordings list
box browser recordings get <recording-id>
box browser recordings download <recording-id> -o session.mp4
```
Chromium starts on first use, so the very first `box browser open` is slower
than the rest, and anything talking to CDP directly fails until it has run once.
`--tab <id>` is optional while one tab is open and required once there are
several. `screenshot` writes to a file because stdout carries text, and that file
lands on this machine rather than in the box. To put a screenshot on a pull request
or issue, see "Attaching a screenshot to a pull request or issue".
Pull structured data off the page with a flat JSON Schema file:
```bash
echo '{"type":"object","properties":{"price":{"type":"string"}},"required":["price"]}' > s.json
box browser extract "the listed price" --schema s.json
```
A property not named in `required` is optional. Nested objects are refused.
### Capturing straight into the box
`box browser screenshot --full-page -o page.png` is the short way, and it is
enough whenever the image can live on this machine. It writes to the machine
running the CLI, though, so getting the image into the box costs an upload.
Chromium's CDP is open on `127.0.0.1:9222` **from inside the box** with no
token, so a script running there captures and writes in one step, and can clip
to a single element, which the CLI does not expose:
Write the script with `box files write` rather than inlining it: the remote
shell is `sh`, and quoting a program through `box exec` is where this goes
wrong.
```bash
box files write shot.mjs - <<'JS'
const targets = await (await fetch("http://127.0.0.1:9222/json")).json();
const page = targets.find((t) => t.type === "page");
const ws = new WebSocket(page.webSocketDebuggerUrl);
await new Promise((r) => (ws.onopen = r));
let id = 0;
const pending = new Map();
ws.onmessage = (m) => {
const msg = JSON.parse(m.data);
pending.get(msg.id)?.(msg.result);
pending.delete(msg.id);
};
const send = (method, params = {}) =>
new Promise((resolve) => {
const callId = ++id;
pending.set(callId, resolve);
ws.send(JSON.stringify({ id: callId, method, params }));
});
// captureBeyondViewport only permits capture outside the viewport; the clip is
// what makes it the whole page. cssContentSize is in CSS pixels, which is what
// clip expects.
const metrics = await send("Page.getLayoutMetrics");
const size = metrics.cssContentSize ?? metrics.contentSize;
const { data } = await send("Page.captureScreenshot", {
format: "png",
captureBeyondViewport: true,
clip: { x: 0, y: 0, width: size.width, height: size.height, scale: 1 },
});
const fs = await import("node:fs");
fs.writeFileSync("shot.png", Buffer.from(data, "base64"));
ws.close();
JS
box exec -- 'node shot.mjs' # shot.png is now in the box
```
The `clip` is what makes this a full-page capture rather than a viewport one;
`--full-page` does the same thing. An element-clipped capture is the same call
with that element's box as the clip, and that one has no CLI flag. Node's global
`fetch` and `WebSocket` are enough, so nothing has to be installed, but Chromium
must have been started once by a `box browser` command first.
This is only worth it when the image should stay in the box or you need a
capture the CLI cannot make. Otherwise `screenshot -o` then `files upload` is
shorter.
## Schedules
Cron on the box, in UTC. Nothing inside the container can register one.
```bash
box schedule exec --cron '0 9 * * *' -- npm run backup
box schedule agent --cron '@daily' "summarise yesterday's errors"
box schedule list
box schedule get <schedule-id> # includes run and failure counts
box schedule pause <schedule-id>
box schedule resume <schedule-id>
box schedule update <schedule-id> --cron '0 10 * * *'
box schedule delete <schedule-id>
```
`update` changes only what you name, so setting the cron leaves the command alone.
## Box configuration
```bash
box skills add upstash-redis-js # skills available to the box's agent
box skills list
box skills remove upstash-redis-js
box config model anthropic/claude-sonnet-5
box config init-command set "npm ci" # keep-alive boxes only; runs on start
box config init-command get
box config init-command delete
box config network deny-all # or allow-all, or custom
box config network custom --allow-domain api.example.com
box config harness --command my-agent # a custom agent harness
```
Account-level settings, which apply to boxes you create later rather than to
this one:
```bash
box env set KEY VAL # applies to boxes created after this
box env list
box env delete KEY
box env set-all A=1 B=2 # replaces every var, does not merge
box labels add staging # then: box list --label staging
box labels list
box labels remove staging
```
Both `box env set` and `box create --env` take the value as an argument, so a
secret passed either way is visible in `ps` and lands in shell history. Neither
is a secrets mechanism; keep real credentials out of both and use a token the
box fetches for itself.
## Flag reference
The flags the walkthroughs above do not reach. Every command also takes the
global `--box`, `--json` and `--token`.
```bash
box create --no-repl --git-user-name N --git-user-email E # commit identity
box create --no-repl --agent-api-key stored # key saved in the console
box create --no-repl --no-use # do not write .box
box init-demo --directory my-demo # scaffold elsewhere
box exec -C /srv/app -- npm test # -C/--cwd: working directory
box run --timeout 600 -q "..." # -q/--quiet: no tool-call logs on stderr
box code --timeout 120 "..." # both take --timeout in seconds
box files read --offset 0 --length 65536 big.log # a slice; 8 MiB per read
box files read --encoding base64 logo.png # binary out
box files write --encoding base64 logo.png - # binary in
box files remove -r build/ # -r required for a directory
box files mkdir -p a/b/c # -p creates missing parents
box files stat --follow link # resolve a final symlink
box git clone --branch main --depth 1 <url> # shallow, single branch
box git clone --github-token $TOKEN <url> # private repository
box git commit -m "msg" --author-name N --author-email E
box git push --branch feature/x # names the branch to push
box public-url 3000 --bearer-token # or --basic-auth; both generate credentials
box use --unset # drop this directory's .box, never a parent's
box schedule agent --cron "0 9 * * *" --model <m> --timeout 300 --webhook-url <url> "..."
box schedule update <id> --timeout 0 # 0 clears the timeout; --prompt, --cron, --model too
box config network custom --allow-domain a.test --allow-cidr 10.0.0.0/8 --deny-cidr 10.1.0.0/16
box config harness --command my-agent --arg --verbose # --arg repeatable, sent before the prompt
```
`-C` means the working directory on `box exec` (`--cwd`) and the repository
directory on every `box git` and `box schedule` subcommand (`--folder`).
## Output
Data goes to stdout, diagnostics to stderr, so piping is safe. `--json` prints the
result as JSON with no wrapper, on every command that returns data. The ones that
open a REPL or print a shell script (`connect`, `init-demo`, `completion`)
reject it rather than answering an automation caller with a prompt:
```bash
box files list --json | jq -r '.[].name'
box get "$(cat .box)" --json
```
`box init-demo` and `box completion` exist but are for people, not agents: one
scaffolds a local demo project, the other prints a shell completion script.
upstash-box-js31.6 KB
---
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, snapshots, and a headless browser. Use when building with Upstash Box, creating a sandbox or isolated environment to run untrusted or agent-generated code, running AI coding agents in containers, giving an agent a cloud dev environment with a shell and repository, browser automation from a box, scheduling recurring jobs inside a box, saving and restoring snapshots, or orchestrating parallel boxes.
license: MIT
metadata:
author: Upstash
homepage: https://upstash.com
---
# @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
```bash
npm install @upstash/box
npm install zod # peer dependency, only needed for responseSchema / browser schemas
```
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
```ts
import { Box, Agent, ClaudeCode, BoxApiKey } from "@upstash/box"
// Create with agent + git + env vars
const box = await Box.create({
name: "my-box",
runtime: "node", // "node" | "python" | "golang" | "ruby" | "rust" (+ "-alpine" variants)
size: "small", // "small" (2 CPU/4GB) | "medium" (4/8) | "large" (8/16)
labels: ["beta", "x-team"], // max 5, ≤20 chars each
keepAlive: true, // don't idle-pause the box
initCommand: "npm install && npm run dev", // keep-alive boxes only
browser: true, // provision headless Chromium for box.browser
agent: {
harness: Agent.ClaudeCode, // Agent.Codex | Agent.OpenCode | Agent.Cursor | Agent.Custom
model: ClaudeCode.Sonnet_4_5,
// apiKey options:
// omit → server decides which key to use
// BoxApiKey.UpstashKey → use Upstash-provided LLM key
// BoxApiKey.StoredKey → use key previously stored via Upstash Console
// "sk-..." → direct API key string
apiKey: BoxApiKey.UpstashKey,
},
git: { // all fields optional
token: process.env.GITHUB_TOKEN, // alternatively link your GitHub account via Upstash Console
userName: "Bot",
userEmail: "bot@example.com",
},
env: { DATABASE_URL: "..." },
skills: ["upstash/qstash-js/qstash-js"], // owner/repo/skill-name
timeout: 600_000, // request timeout in ms
debug: false,
})
// Reconnect, list, delete, pause/resume
// Box.get / Box.getByName take { apiKey, baseUrl, gitToken, timeout, debug }
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" }) // filter by label
await box.pause() // throws on keep-alive boxes — they are never idle-paused
await box.resume()
await box.delete() // irreversible
const { status } = await box.getStatus()
box.id; box.size; box.keepAlive; box.cwd; box.networkPolicy
// Init command (keep-alive boxes only — throws otherwise)
await box.setInitCommand("npm run dev")
const script = await box.getInitCommand()
await box.deleteInitCommand()
// Bulk delete (static, by ID)
await Box.delete({ boxIds: ["box_1", "box_2"] })
const { deleted } = await Box.deleteSnapshots({ snapshotIds: ["snap_1"] }) // omit ids → delete all
```
### Account-level env vars
Injected into every box you create.
```ts
await Box.setEnv("API_TOKEN", "secret")
const env = await Box.listEnv() // values are masked
await Box.setAllEnv({ A: "1", B: "2" }) // full replace — unlisted keys are removed
await Box.deleteEnv("API_TOKEN")
```
## Agent Runs
```ts
import { z } from "zod"
// Structured output with Zod schema
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" }, // harness-specific
onToolUse: (tool) => console.log(tool.name, tool.input),
onToolResult: (result) => console.log(result.toolCallId, result.output),
})
run.status // "running" | "completed" | "failed" | "cancelled" | "detached"
run.result // typed from schema
run.cost // { inputTokens, outputTokens, cachedInputTokens, computeMs, totalUsd }
// Attach files to a prompt (max 10 files, 10 MB each)
await box.agent.run({ prompt: "Describe this", files: ["./screenshot.png"] })
await box.agent.run({
prompt: "Describe this",
files: [{ data: base64, mediaType: "image/png", filename: "shot.png" }],
})
// Streaming — chunk is a discriminated union
const stream = await box.agent.stream({ prompt: "Build a REST API" })
for await (const chunk of stream) {
if (chunk.type === "text-delta") process.stdout.write(chunk.text)
if (chunk.type === "reasoning") process.stdout.write(chunk.text)
if (chunk.type === "tool-call") console.log(chunk.toolName, chunk.input)
if (chunk.type === "tool-result") console.log(chunk.output)
if (chunk.type === "finish") console.log(chunk.output, chunk.usage, chunk.sessionId)
// also: { type: "start", runId } | { type: "stats", cpuNs, memoryPeakBytes } | { type: "unknown" }
}
stream.status // "completed" after iteration finishes
stream.result // final output
// stream() takes the same prompt/files/options/timeout/onToolUse/onToolResult as run().
// It has no responseSchema, maxRetries, or webhook — use run() for those.
// Fire-and-forget with webhook
await box.agent.run({
prompt: "Run tests",
webhook: { url: "https://example.com/hook", headers: { Authorization: "Bearer ..." } },
})
```
### 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.
```ts
// Agent.ClaudeCode → ClaudeCodeAgentOptions
{
maxTurns: 20,
maxBudgetUsd: 1.0,
effort: "high", // "low" | "medium" | "high" | "max"
thinking: { type: "adaptive" }, // | { type: "enabled", budgetTokens: 8000 } | { type: "disabled" }
disallowedTools: ["Bash"],
agents: { reviewer: { /* custom subagent definition */ } },
promptSuggestions: false,
fallbackModel: "anthropic/claude-sonnet-4-5",
systemPrompt: "You are a release engineer.",
}
// Agent.Codex → CodexAgentOptions
{
modelReasoningEffort: "high", // "none" | "minimal" | "low" | "medium" | "high" | "xhigh"
modelReasoningSummary: "concise", // "auto" | "concise" | "detailed" | "none"
personality: "pragmatic", // "friendly" | "pragmatic" | "none"
webSearch: "live", // or true / false
}
// Agent.OpenCode → OpenCodeAgentOptions
{
reasoningEffort: "high", // "low" | "medium" | "high"
textVerbosity: "low", // "low" | "medium" | "high"
reasoningSummary: "auto", // "auto" | "concise" | "detailed" | "none"
thinking: { type: "enabled", budgetTokens: 8000 }, // Anthropic-backed models
}
// Agent.Cursor → free-form Record<string, unknown>
```
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.
```ts
import { ClaudeCode, OpenAICodex, OpenCodeModel, CursorModel, OpenRouterModel, VercelModel } from "@upstash/box"
ClaudeCode.Fable_5_1 // "anthropic/claude-fable-5-1"
ClaudeCode.Opus_5 // "anthropic/claude-opus-5"
ClaudeCode.Sonnet_5 // "anthropic/claude-sonnet-5"
OpenAICodex.GPT_6_Astra // "openai/gpt-6-astra"
OpenAICodex.GPT_5_6 // "openai/gpt-5.6"
OpenCodeModel.Claude_Opus_5 // "opencode/claude-opus-5"
CursorModel.Composer_2_5 // "cursor/composer-2.5"
OpenRouterModel.Claude_Opus_5 // "openrouter/anthropic/claude-opus-5"
VercelModel.GPT_5_5 // "vercel/openai/gpt-5.5"
// Read / change the box's harness + model at runtime
const { harness, model } = box.modelConfig
await box.configureModel("anthropic/claude-opus-4-8")
// Which harness a bare model string implies (prefix-based)
import { inferDefaultProvider } from "@upstash/box"
inferDefaultProvider("openai/gpt-5.6") // Agent.Codex
inferDefaultProvider("cursor/default") // Agent.Cursor
```
### Custom harness
Run your own agent binary inside the box instead of a managed harness.
```ts
import { Box, Agent, runCustomHarness } from "@upstash/box"
const box = await Box.create({
agent: {
harness: Agent.Custom,
model: "my-agent", // label forwarded to the process
// command: name on PATH, or an absolute path under /workspace/home or /home/boxuser
customHarness: { command: "node", args: ["/workspace/home/agent.js"], protocol: "box-sse-v1" },
},
})
await box.configureCustomHarness({ command: "node", args: ["/workspace/home/agent2.js"] })
// Inside the box, agent.js emits box-sse-v1 events. The backend appends
// `-p <prompt> --model <model> --stream` (+ `--session <id>` when resuming).
await runCustomHarness(async ({ prompt, model, sessionId, stream, args }, emit) => {
emit.text("working...")
emit.reasoning("thinking out loud") // -> `thinking` event
emit.tool({ toolCallId: "1", name: "Bash", input: { command: "ls" } })
emit.toolResult({ toolCallId: "1", output: "file.txt" })
emit.emit("custom-event", { any: "payload" }) // raw escape hatch
// emit.error(new Error("boom")) to fail the run
return {
output: "done",
inputTokens: 10,
outputTokens: 5,
cachedInputTokens: 0,
totalCostUsd: 0.01,
sessionId,
} // returning a plain string is shorthand for { output }
})
```
## Run Fields
Every `run` (agent, command, or code) returns a `Run<T>`:
```ts
const run = await box.exec.command("npm test")
run.id // run ID
run.status // "completed" | "failed" | ...
run.result // stdout on success, stderr on failure (or typed T with responseSchema)
run.stdout // raw stdout (command/code runs)
run.stderr // raw stderr (command/code runs)
run.exitCode // number | null (null for agent runs)
run.cost // { inputTokens, outputTokens, cachedInputTokens, computeMs, totalUsd }
await run.cancel() // cancel a running run
const logs = await run.logs() // [{ timestamp, level, message }]
// Box-level history
const entries = await box.logs({ limit: 100, offset: 0 }) // [{ timestamp, level, source, message }]
const runs = await box.listRuns() // backend run records, newest first
```
## Shell Execution
```ts
// Run commands
const run = await box.exec.command("echo hello && ls -la")
// Run code snippets — lang: "js" | "ts" | "python"
const run2 = await box.exec.code({ code: "console.log(1+1)", lang: "js", timeout: 10_000 })
// Streaming shell / code
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) {
// chunk: { type: "output", data } | { type: "exit", exitCode, cpuNs }
}
```
### Live sessions
`exec.session()` opens a WebSocket to a process that is *still running* — stdin,
streamed stdout/stderr, PTY resize, and signals. Node-only: auth is a handshake
header, which browsers cannot set (`ws` ships as an SDK dependency, nothing to
install). Available on `Box` and `EphemeralBox`.
```ts
const session = await box.exec.session({
cmd: "sort", // run via `bash -lc`; `argv: ["sort"]` runs the program with
// no shell and takes precedence over cmd
cwd: "/workspace/home", // defaults to the box's tracked cwd
env: ["LOG_LEVEL=debug"], // KEY=VALUE entries overlaid on the box environment
onStdout: (bytes) => process.stdout.write(bytes), // Uint8Array
onStderr: (bytes) => process.stderr.write(bytes), // separate stream unless tty
})
session.pid // in-box PID, always non-zero
session.execId // server-side exec id
session.write("banana\napple\n")
session.endStdin() // EOF — a command that reads to EOF now exits by itself
const exitCode = await session.wait() // -1 if torn down while still running
session.close() // hang up; also kills the process
// Interactive programs / TUIs — tty allocates a real PTY, merging stderr into stdout
const shell = await box.exec.session({ argv: ["bash", "-i"], tty: true, rows: 40, cols: 120 })
shell.resize(50, 160)
shell.kill("INT") // allowlist: TERM KILL INT HUP TSTP QUIT USR1 USR2 (default TERM)
shell.terminate(5000) // server-side SIGTERM, then SIGKILL after the grace (first call wins)
```
The session owns the process: `close()`, a dropped connection, or your process
exiting all kill the command, and a session cannot be reattached. Use `wait()`
to run something to completion.
## Filesystem
```ts
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") // [{ name, path, size, is_dir, mod_time }]
// Binary files — use encoding: "base64" for read and write
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" })
// Bounded byte-range read — the *presence* of `length` selects the range, so
// { length: 0 } reads zero bytes rather than the whole file. Server caps it at 8 MiB.
const head = await box.files.read("/workspace/home/big.log", { length: 64 * 1024 })
const slice = await box.files.read("/workspace/home/big.log", { offset: 1024, length: 512 })
// Metadata — defaults to lstat, so a symlink reports type "symlink"
const stat = await box.files.stat("/workspace/home/app.js")
// stat: { type: "file" | "directory" | "symlink" | "other", size, mod_time, inode, version }
// `version` is an opaque freshness token (inode + mtime + size) for optimistic-concurrency
// guards — compare it for equality, never parse it.
const target = await box.files.stat("/workspace/home/link", { follow: true }) // dereference
// Directories, moves, deletes
await box.files.mkdir("build/cache", { parents: true }) // parents mirrors `mkdir -p`
await box.files.rename("draft.md", "docs/final.md") // move/rename
await box.files.remove("build/cache", { recursive: true }) // recursive required for a directory
// Upload local files
await box.files.upload([{ path: "./local/file.txt", destination: "/workspace/home/file.txt" }])
// Download — `folder` is a path INSIDE the box; files land in ./<basename>
await box.files.download({ folder: "src" }) // → ./src
await box.files.download() // whole cwd → ./workspace
```
## cd / Working Directory
The SDK tracks `cwd` client-side. All operations (exec, files, git, agent) run relative to it.
```ts
box.cwd // current working directory (starts at /workspace/home)
await box.cd("my-repo") // relative to current cwd
await box.cd("/workspace/home/other") // absolute path
```
## 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.
Every git call except `clone` runs in the box's current directory, so `cd` into the
clone first. At the workspace root there is no repository, and `status` comes back
empty, which reads as a clean tree.
```ts
await box.git.clone({ repo: "github.com/org/repo", branch: "main" })
await box.git.clone({ repo: "github.com/org/repo", depth: 1 }) // shallow clone
await box.git.clone({ repo: "github.com/org/repo", folder: "my-app" }) // destination
await box.cd("repo") // the clone lands in a directory named after the 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", // optional per-commit override
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: "Fix bug", body: "...", base: "main" })
// pr: { url, number, title, base }
// Update the box-wide git identity
const cfg = await box.git.updateConfig({ userName: "Bot", userEmail: "bot@example.com" })
// cfg: { git_user_name, git_user_email }
// Arbitrary git commands. Check exit_code: 128 means the cwd is not a repository.
const { output, exit_code } = await box.git.exec({ args: ["log", "--oneline", "-5"] })
```
## Schedules
Cron tasks on a box — shell commands or agent prompts. Available on `Box` and `EphemeralBox`. Cron is UTC.
```ts
const execSchedule = await box.schedule.exec({
cron: "* * * * *",
command: ["bash", "-c", "date >> /workspace/home/cron.log"],
folder: "/workspace/home", // optional cwd override
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", // optional cwd override
model: "anthropic/claude-sonnet-5", // optional override
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.schedule.get(agentSchedule.id)
// Partial update — omitted fields keep their value, "" / [] / {} clear a field,
// `options: null` clears agent options. The schedule's type cannot change.
// Updatable: cron, command, prompt, folder, model, options, timeout, webhookUrl, webhookHeaders
await box.schedule.update(agentSchedule.id, { cron: "0 18 * * *", webhookUrl: "" })
await box.schedule.pause(agentSchedule.id)
await box.schedule.resume(agentSchedule.id)
await box.schedule.delete(agentSchedule.id)
```
## Snapshots
```ts
// Snapshot — checkpoint workspace state
const snap = await box.snapshot({ name: "after-setup" })
// snap: { id, name, box_id, size_bytes, status, created_at }
// fromSnapshot takes a BoxConfig: name, labels, size, keepAlive, initCommand, runtime,
// agent, git, env, attachHeaders, networkPolicy. It does NOT send `browser`, `skills`,
// or `mcpServers` (the Python SDK does) — add skills with box.skills.add() afterwards,
// and use Box.create({ browser: true }) when you need Chromium.
const restored = await Box.fromSnapshot(snap.id, {
size: "medium",
keepAlive: true,
// git identity is forwarded, not just the token
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.
```ts
import { z } from "zod"
const box = await Box.create({
browser: true,
agent: { harness: Agent.ClaudeCode, model: ClaudeCode.Sonnet_4_5 },
})
// Tabs
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) // no network call
tab.id; tab.url; tab.title // handle metadata, no network call
// Page operations
const content = await tab.goto("https://news.ycombinator.com") // { title, url, text, links }
const current = await tab.content()
const png = await tab.screenshot() // Uint8Array
const b64 = await tab.screenshot({ type: "base64", fullPage: true })
// AI operations (metered) — extract/observe/act take an optional { model } override,
// defaulting to the box's model (or anthropic/claude-sonnet-4-5 when it has none)
const data = await tab.extract(
"Top story title and points",
z.object({ title: z.string(), points: z.number() }),
{ model: "anthropic/claude-sonnet-4-5" },
)
// observe → actionable elements, each carrying a replayable method + arguments
const { elements } = await tab.observe("What can I click?", { model: "openai/gpt-5.6" })
// elements: [{ description, selector?, url?, method?, arguments? }]
const acted = await tab.act("Click the first headline")
// acted: { success, message, actionDescription, actions, cacheStatus?, inputTokens, outputTokens }
// Replay a pre-resolved action — no LLM call, no tokens, no model provider key.
// Takes a BrowserAction (= BrowserObserveElement | BrowserActAction); `model` is
// ignored in this form, and an action without a `selector` throws.
await tab.act(elements[0])
await tab.act(acted.actions[0])
// Live view + raw CDP
const liveUrl = await tab.liveViewUrl() // view-only screencast page/iframe
const cdpUrl = await box.browser.cdpUrl() // wss://…?token=… — no extra auth wiring
await tab.close()
// Drive the same browser from Playwright / Puppeteer / Stagehand
import { chromium } from "playwright-core"
const remote = await chromium.connectOverCDP(cdpUrl)
const context = remote.contexts()[0] ?? (await remote.newContext())
const page = context.pages()[0] ?? (await context.newPage())
await page.goto("https://example.com")
// Stagehand: new Stagehand({ env: "LOCAL", localBrowserLaunchOptions: { cdpUrl } })
// Session recordings (HLS playback URL + MP4 download, chapter markers).
// One active recording per box; captures all tabs and follows the foreground.
// Auto-stops after maxDurationSeconds or ~3 minutes of no on-screen activity.
const handle = await box.browser.recordings.start({ maxDurationSeconds: 600 }) // default & max 600
const recording = await handle.stop()
// or stop whatever is recording on the box, without a handle:
// const recording = await box.browser.recordings.stop()
// recording: { id, boxId, status, startedAt, endedAt, durationMs, sizeBytes, mp4SizeBytes,
// segmentCount, markers, stoppedReason, maxDurationSeconds, expiresAt, playlistUrl }
// markers: { type: "tab_switch", atMs, endMs?, label?, tabId? }
// expiresAt is epoch ms (videos retained 14 days); playlistUrl is API-served — fetch it
// with an `X-Box-Api-Key: <apiKey>` header (hls.js / Safari / ffplay).
const all = await box.browser.recordings.list()
const one = await box.browser.recordings.get(recording.id)
// Download the video to a local file — returns the path written.
// Defaults to ./box-recording-<id>.mp4 (.ts for recordings captured before MP4 support).
const file = await box.browser.recordings.download(recording.id)
await box.browser.recordings.download(recording.id, { path: "./out/demo.mp4" })
```
### 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.
```ts
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) // deterministic reset, no browser-AI tokens
await tab.act(action) // replay the resolved click: no LLM, no tokens
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.
```ts
import { EphemeralBox } from "@upstash/box"
const ebox = await EphemeralBox.create({
name: "scratch-box",
runtime: "python",
size: "small",
ttl: 3600, // seconds, max 259200 (3 days), default 259200
env: { API_KEY: "..." },
labels: ["scratch"], // settable at create time; filter via Box.list({ label })
networkPolicy: { mode: "deny-all" },
attachHeaders: { "api.stripe.com": { Authorization: "Bearer sk_live_..." } },
})
ebox.networkPolicy
ebox.expiresAt // unix timestamp when auto-deleted
await ebox.exec.command("python -c 'print(1+1)'")
await ebox.exec.code({ code: "print('hi')", lang: "python" })
await ebox.exec.session({ argv: ["bash", "-i"], tty: true }) // whole exec namespace, session included
await ebox.files.write({ path: "/workspace/home/data.json", content: "{}" })
await ebox.files.stat("/workspace/home/data.json") // whole files namespace, stat/mkdir/rename/remove included
await ebox.schedule.exec({ cron: "* * * * *", command: ["bash", "-c", "date"] })
await ebox.cd("subdir")
const snap = await ebox.snapshot({ name: "checkpoint" })
await ebox.listSnapshots()
await ebox.deleteSnapshot(snap.id)
const { status } = await ebox.getStatus()
await ebox.delete()
// Restore from snapshot
const ebox2 = await EphemeralBox.fromSnapshot(snap.id, { ttl: 7200 })
// Statics: EphemeralBox.delete({ boxIds }) and EphemeralBox.deleteSnapshots() are the
// Box ones. EphemeralBox.getByName() is Box.get — it returns a full `Box`, not an
// `EphemeralBox` (quirk mirrored in the Python SDK).
```
## Public URLs
Expose box ports as public URLs with optional auth.
```ts
const publicURL = await box.getPublicURL(3000)
// publicURL: { url: "https://{id}-3000.preview.box.upstash.com", port }
const authed = await box.getPublicURL(3000, { bearerToken: true })
// authed: { url, port, token }
const basic = await box.getPublicURL(3000, { basicAuth: true })
// basic: { url, port, username, password }
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.
```ts
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
```ts
const labels = await box.labels.add("prod") // returns the updated set
await box.labels.remove("beta")
const current = await box.labels.list()
const prodBoxes = await Box.list({ label: "prod" })
```
## Network Policy & Outbound Headers
```ts
const box = await Box.create({
// mode: "allow-all" (default) | "deny-all" | "custom"
// custom takes any of allowedDomains / allowedCidrs / deniedCidrs
networkPolicy: {
mode: "custom",
allowedDomains: ["api.example.com"],
allowedCidrs: ["203.0.113.0/24"],
deniedCidrs: ["10.0.0.0/8"],
},
// Inject secret headers into matching outbound HTTPS requests (write-only, never read back)
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.
```ts
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
```ts
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):
```bash
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
- `files.read()` slices only when `length` is present — `{ offset }` alone reads the whole file, and `{ length: 0 }` reads nothing
- `files.stat()` is an lstat by default: a symlink reports `type: "symlink"` unless you pass `{ follow: true }`
- `files.remove()` needs `{ recursive: true }` for a directory, and `files.mkdir()` needs `{ parents: true }` for nested paths
- `exec.session()` is Node-only (the WebSocket handshake carries an auth header) and the handle owns the process — `close()` or a dropped connection kills the command, and sessions cannot be reattached
- `exec.session({ tty: true })` merges stderr into stdout, so `onStderr` never fires for a PTY session
- `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
- `Box.fromSnapshot()` creates a new box — it does not modify the original, and it does not forward `browser`, `skills`, or `mcpServers` from the config you pass
- `EphemeralBox` has no `updateNetworkPolicy` — set `networkPolicy` at create time
- `responseSchema` and browser `schema` need `zod` installed (peer dependency, v3 or v4)
- All `timeout` values are milliseconds
upstash-box-py33.6 KB
---
name: upstash-box-py
description: Work with the upstash-box Python SDK for sandboxed cloud containers with AI agents, shell, filesystem, git, cron schedules, snapshots, and a headless browser. Use when building with Upstash Box in Python, creating a sandbox or isolated environment to run untrusted or agent-generated code, running AI coding agents in containers, giving an agent a cloud dev environment with a shell and repository, browser automation from a box, scheduling recurring jobs inside a box, saving and restoring snapshots, or orchestrating parallel boxes.
license: MIT
metadata:
author: Upstash
homepage: https://upstash.com
---
# upstash-box Python SDK
Sandboxed cloud containers with built-in AI agents, shell, filesystem, git, cron schedules, and an optional headless browser.
Mirrors the `@upstash/box` TypeScript SDK (`upstash-box-js` skill) with
snake_case names; the intentional differences are listed under Gotchas.
## Install & Setup
```bash
pip install upstash-box
```
Set `UPSTASH_BOX_API_KEY` env var or pass `api_key` to constructors.
The SDK ships both a synchronous `Box` (used in the examples below) and an
asynchronous `AsyncBox` (`box = await AsyncBox.create(...)`, `await box.agent.run(...)`).
The async surface is identical with `await` and `async for`.
Anonymous telemetry headers are sent with every request; opt out with the
`UPSTASH_DISABLE_TELEMETRY` env var.
## Box Lifecycle
```python
import os
from upstash_box import Box, Agent, ClaudeCode, BoxApiKey
# Create with agent + git + env vars
box = Box.create(
name="my-box",
runtime="node", # "node" | "python" | "golang" | "ruby" | "rust" (+ "-alpine" variants)
size="small", # "small" (2 CPU/4GB) | "medium" (4/8) | "large" (8/16)
labels=["beta", "x-team"], # max 5, <=20 chars each
keep_alive=True, # don't idle-pause the box
init_command="npm install && npm run dev", # keep-alive boxes only
browser=True, # provision headless Chromium for box.browser
agent={
"harness": Agent.CLAUDE_CODE, # Agent.CODEX | Agent.OPEN_CODE | Agent.CURSOR | Agent.CUSTOM
"model": ClaudeCode.SONNET_4_5, # or a plain string "anthropic/claude-sonnet-4-5"
# api_key options:
# omit → server decides which key to use
# BoxApiKey.UPSTASH_KEY → use Upstash-provided LLM key
# BoxApiKey.STORED_KEY → use key previously stored via Upstash Console
# "sk-..." → direct API key string
"api_key": BoxApiKey.UPSTASH_KEY,
},
git={ # all fields optional
"token": os.environ["GITHUB_TOKEN"], # or link your GitHub account via Upstash Console
"user_name": "Bot",
"user_email": "bot@example.com",
},
env={"DATABASE_URL": "..."},
skills=["upstash/qstash-js/qstash-js"], # owner/repo/skill-name
timeout=600_000, # request timeout in ms
debug=False,
)
# Reconnect, list, delete, pause/resume
# Box.get / Box.get_by_name take api_key, base_url, git_token, timeout, debug
same = Box.get(box.id, git_token="ghp_...") # git_token, not git={...}, when reconnecting
by_name = Box.get_by_name("my-box")
all_boxes = Box.list()
beta = Box.list(label="beta") # filter by label
box.pause() # raises on keep-alive boxes — they are never idle-paused
box.resume()
box.delete() # irreversible
status = box.get_status()["status"]
box.id, box.size, box.keep_alive, box.cwd, box.network_policy
# Init command (keep-alive boxes only — raises otherwise)
box.set_init_command("npm run dev")
script = box.get_init_command()
box.delete_init_command()
# Bulk delete (classmethods, by ID)
Box.delete_boxes(box_ids=["box_1", "box_2"]) # JS static `delete` is `delete_boxes` here
Box.delete_snapshots(snapshot_ids=["snap_1"]) # omit ids → delete all
```
### Account-level env vars
Injected into every box you create.
```python
Box.set_env("API_TOKEN", "secret")
env = Box.list_env() # values are masked
Box.set_all_env({"A": "1", "B": "2"}) # full replace — unlisted keys are removed
Box.delete_env("API_TOKEN")
```
## Agent Runs
```python
from pydantic import BaseModel
# Structured output with a Pydantic model (or a raw JSON-schema dict)
class Finding(BaseModel):
severity: str # "high" | "medium" | "low"
file: str
issue: str
class Review(BaseModel):
verdict: str # "approved" | "changes_requested"
findings: list[Finding]
run = box.agent.run(
prompt="Review the code for security issues",
response_schema=Review,
timeout=120_000,
max_retries=2,
options={"max_turns": 20, "max_budget_usd": 1.0, "effort": "high"}, # harness-specific
on_tool_use=lambda tool: print(tool["name"], tool["input"]),
on_tool_result=lambda result: print(result["tool_call_id"], result["output"]),
)
run.status # "running" | "completed" | "failed" | "cancelled" | "detached"
run.result # typed from schema (a Review instance)
run.cost # RunCost(input_tokens, output_tokens, cached_input_tokens, compute_ms, total_usd)
# Attach files to a prompt (max 10 files, 10 MB each)
box.agent.run(prompt="Describe this", files=["./screenshot.png"])
box.agent.run(
prompt="Describe this",
files=[{"data": b64, "media_type": "image/png", "filename": "shot.png"}],
)
# Streaming — chunks are typed dataclasses discriminated on `.type`
stream = box.agent.stream(prompt="Build a REST API")
for chunk in stream:
if chunk.type == "text-delta":
print(chunk.text, end="")
elif chunk.type == "reasoning":
print(chunk.text, end="")
elif chunk.type == "tool-call":
print(chunk.tool_name, chunk.input)
elif chunk.type == "tool-result":
print(chunk.output)
elif chunk.type == "finish":
print(chunk.usage.input_tokens, chunk.usage.cached_input_tokens, chunk.session_id)
# also: StartChunk(run_id) | StatsChunk(cpu_ns, memory_peak_bytes) | UnknownChunk(event, data)
# FinishChunk also carries .output (the final text)
# stream() takes the same prompt/files/options/timeout/on_tool_use/on_tool_result as run().
# It has no response_schema, max_retries, or webhook — use run() for those.
# Fire-and-forget with webhook
box.agent.run(
prompt="Run tests",
webhook={"url": "https://example.com/hook", "headers": {"Authorization": "Bearer ..."}},
)
```
### Agent options (per harness)
`options` is forwarded to the harness — the accepted keys depend on which one
the box runs. Keys are snake_case in Python; the SDK converts **top-level** keys
to each harness's backend casing (Claude Code / OpenCode → camelCase, Codex →
snake_case). Keys inside nested dicts are sent verbatim.
```python
# Agent.CLAUDE_CODE → ClaudeCodeAgentOptions
{
"max_turns": 20,
"max_budget_usd": 1.0,
"effort": "high", # "low" | "medium" | "high" | "max"
# nested dicts are forwarded verbatim — keep `budgetTokens` camelCase here
"thinking": {"type": "adaptive"}, # or {"type": "enabled", "budgetTokens": 8000} / {"type": "disabled"}
"disallowed_tools": ["Bash"],
"agents": {"reviewer": {...}}, # custom subagent definitions
"prompt_suggestions": False,
"fallback_model": "anthropic/claude-sonnet-4-5",
"system_prompt": "You are a release engineer.",
}
# Agent.CODEX → CodexAgentOptions
{
"model_reasoning_effort": "high", # "none" | "minimal" | "low" | "medium" | "high" | "xhigh"
"model_reasoning_summary": "concise", # "auto" | "concise" | "detailed" | "none"
"personality": "pragmatic", # "friendly" | "pragmatic" | "none"
"web_search": "live", # or True / False
}
# Agent.OPEN_CODE → OpenCodeAgentOptions
{
"reasoning_effort": "high", # "low" | "medium" | "high"
"text_verbosity": "low", # "low" | "medium" | "high"
"reasoning_summary": "auto", # "auto" | "concise" | "detailed" | "none"
"thinking": {"type": "enabled", "budgetTokens": 8000}, # Anthropic-backed models
}
# Agent.CURSOR → free-form dict
```
Unlike the JS generic `AgentOptions<TProvider>`, Python does not narrow
`options` by harness — the type is the union of all shapes plus a raw dict.
### Harness & model
`harness` is required. Model enums: `ClaudeCode`, `OpenAICodex`, `OpenCodeModel`,
`CursorModel`, `OpenRouterModel`, `VercelModel` — or any provider-prefixed string.
```python
from upstash_box import ClaudeCode, OpenAICodex, OpenCodeModel, CursorModel, OpenRouterModel, VercelModel
ClaudeCode.FABLE_5_1 # "anthropic/claude-fable-5-1"
ClaudeCode.OPUS_5 # "anthropic/claude-opus-5"
ClaudeCode.SONNET_5 # "anthropic/claude-sonnet-5"
OpenAICodex.GPT_6_ASTRA # "openai/gpt-6-astra"
OpenAICodex.GPT_5_6 # "openai/gpt-5.6"
OpenCodeModel.CLAUDE_OPUS_5 # "opencode/claude-opus-5"
CursorModel.COMPOSER_2_5 # "cursor/composer-2.5"
OpenRouterModel.CLAUDE_OPUS_5 # "openrouter/anthropic/claude-opus-5"
VercelModel.GPT_5_5 # "vercel/openai/gpt-5.5"
# Read / change the box's harness + model at runtime
box.model_config # {"harness": ..., "model": ...}
box.configure_model("anthropic/claude-opus-4-8")
# Which harness a bare model string implies (prefix-based)
from upstash_box import infer_default_provider
infer_default_provider("openai/gpt-5.6") # Agent.CODEX
infer_default_provider("cursor/default") # Agent.CURSOR
```
### Custom harness
Run your own agent process inside the box instead of a managed harness.
```python
import asyncio
from upstash_box import Agent, Box, CustomHarnessDone, run_custom_harness
box = Box.create(
agent={
"harness": Agent.CUSTOM,
"model": "my-agent", # label forwarded to the process
# command: name on PATH, or an absolute path under /workspace/home or /home/boxuser
"custom_harness": {
"command": "python",
"args": ["/workspace/home/agent.py"],
"protocol": "box-sse-v1", # default
},
},
)
box.configure_custom_harness({"command": "python", "args": ["/workspace/home/agent2.py"]})
# Inside the box, agent.py emits box-sse-v1 events. The backend appends
# `-p <prompt> --model <model> --stream` (+ `--session <id>` when resuming).
# ctx: CustomHarnessContext(prompt, model, stream, args, session_id)
async def handler(ctx, emit):
emit.text("working...")
emit.reasoning("thinking out loud") # -> `thinking` event
emit.tool({"tool_call_id": "1", "name": "Bash", "input": {"command": "ls"}})
emit.tool_result({"tool_call_id": "1", "output": "file.txt"})
emit.emit("custom-event", {"any": "payload"}) # raw escape hatch
# emit.error("boom") to fail the run
return CustomHarnessDone(
output="done",
input_tokens=10,
output_tokens=5,
cached_input_tokens=0,
total_cost_usd=0.01,
session_id=ctx.session_id,
) # returning a plain string is shorthand for CustomHarnessDone(output=...)
asyncio.run(run_custom_harness(handler)) # run_custom_harness is async; handler may be sync or async
```
## Run Fields
Every `run` (agent, command, or code) returns a `Run`:
```python
run = box.exec.command("npm test")
run.id # run ID
run.status # "completed" | "failed" | ...
run.result # stdout on success, stderr on failure (or typed result with response_schema)
run.stdout # raw stdout (command/code runs)
run.stderr # raw stderr (command/code runs)
run.exit_code # int | None (None for agent runs)
run.cost # RunCost(input_tokens, output_tokens, cached_input_tokens, compute_ms, total_usd)
run.cancel() # cancel a running run
logs = run.logs() # [RunLog(timestamp, level, message)]
# Box-level history
entries = box.logs(limit=100, offset=0) # [LogEntry(timestamp, level, source, message)]
runs = box.list_runs() # backend run records, newest first
```
## Shell Execution
```python
# Run commands
run = box.exec.command("echo hello && ls -la")
# Run code snippets — lang: "js" | "ts" | "python"
run2 = box.exec.code(code="print(1 + 1)", lang="python", timeout=10_000)
# Streaming shell / code
stream = box.exec.stream("npm run build")
stream2 = box.exec.stream_code(code="print('hi')", lang="python")
for chunk in stream:
# chunk: ExecOutputChunk(type="output", data) | ExecExitChunk(type="exit", exit_code, cpu_ns)
...
```
### Live sessions
`exec.session()` opens a WebSocket to a process that is *still running* — stdin,
streamed stdout/stderr, PTY resize, and signals. It is the one feature not carried
by `httpx`; the `websockets` dependency is imported lazily, only when a session
opens. Available on `Box`, `AsyncBox`, and both ephemeral clients.
```python
session = box.exec.session(
cmd="sort", # run via `bash -lc`; argv=["sort"] runs the program with
# no shell and takes precedence over cmd
cwd="/workspace/home", # defaults to the box's tracked cwd
env=["LOG_LEVEL=debug"], # KEY=VALUE entries overlaid on the box environment
on_stdout=lambda b: print(b.decode(), end=""), # bytes
on_stderr=lambda b: print(b.decode(), end=""), # separate stream unless tty
)
session.pid # in-box PID, always non-zero
session.exec_id # server-side exec id
session.write("banana\napple\n")
session.end_stdin() # EOF — a command that reads to EOF now exits by itself
exit_code = session.wait() # -1 if torn down while still running; wait(timeout=5) raises TimeoutError
session.close() # hang up; also kills the process
# Interactive programs / TUIs — tty allocates a real PTY, merging stderr into stdout
with box.exec.session(argv=["bash", "-i"], tty=True, rows=40, cols=120) as shell:
shell.resize(50, 160)
shell.kill("INT") # allowlist: TERM KILL INT HUP TSTP QUIT USR1 USR2 (default TERM)
shell.terminate(5000) # server-side SIGTERM, then SIGKILL after the grace (first call wins)
```
The session owns the process: `close()` (or leaving the `with` block), a dropped
connection, or the program exiting all kill the command, and a session cannot be
reattached. Use `wait()` to run something to completion.
On `AsyncBox` every handle method is a coroutine (`await session.write(...)`,
`await session.wait()`, `async with await box.exec.session(...) as s:`) and an
`async` callback is awaited; `wait()` there takes no timeout. In the sync client
the callbacks run on a background reader
thread — keep them short, and never call `wait()` from inside one, since the exit
frame it waits for arrives on the very thread it is blocking.
## Filesystem
```python
box.files.write(path="/workspace/home/app.py", content="print('hi')")
content = box.files.read("/workspace/home/app.py")
entries = box.files.list("/workspace/home") # [FileEntry(name, path, size, is_dir, mod_time)]
# Binary files — use encoding="base64" for read and write
box.files.write(path="/workspace/home/image.png", content=base64_string, encoding="base64")
b64 = box.files.read("/workspace/home/image.png", encoding="base64")
# Bounded byte-range read — the *presence* of `length` selects the range, so
# length=0 reads zero bytes rather than the whole file. Server caps it at 8 MiB.
head = box.files.read("/workspace/home/big.log", length=64 * 1024)
chunk = box.files.read("/workspace/home/big.log", offset=1024, length=512)
# Metadata — defaults to lstat, so a symlink reports type "symlink"
stat = box.files.stat("/workspace/home/app.py")
# stat: FileStat(type="file" | "directory" | "symlink" | "other", size, mod_time, inode, version)
# `version` is an opaque freshness token (inode + mtime + size) for optimistic-concurrency
# guards — compare it for equality, never parse it.
target = box.files.stat("/workspace/home/link", follow=True) # dereference
# Directories, moves, deletes
box.files.mkdir("build/cache", parents=True) # parents mirrors `mkdir -p`
box.files.rename("draft.md", "docs/final.md") # positional (from_path, to_path)
box.files.remove("build/cache", recursive=True) # recursive required for a directory
# Upload local files
box.files.upload([{"path": "./local/file.txt", "destination": "/workspace/home/file.txt"}])
# Download — `folder` is a path INSIDE the box; files land in ./<basename>
box.files.download(folder="src") # → ./src
box.files.download() # whole cwd → ./workspace
```
## cd / Working Directory
The SDK tracks `cwd` client-side. All operations (exec, files, git, agent) run relative to it.
```python
box.cwd # current working directory (starts at /workspace/home)
box.cd("my-repo") # relative to current cwd
box.cd("/workspace/home/other") # absolute path
```
## 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 `network_policy` (see below) before running its build or tests.
Every git call except `clone` runs in the box's current directory, so `cd` into the
clone first. At the workspace root there is no repository, and `status` comes back
empty, which reads as a clean tree.
```python
box.git.clone(repo="github.com/org/repo", branch="main")
box.git.clone(repo="github.com/org/repo", depth=1) # shallow clone
box.git.clone(repo="github.com/org/repo", folder="my-app") # destination
box.cd("repo") # the clone lands in a directory named after the repo
status = box.git.status()
diff = box.git.diff()
result = box.git.commit( # GitCommitResult(sha, message)
message="fix: resolve bug",
author_name="Jane Doe", # optional per-commit override
author_email="jane@example.com",
)
box.git.push(branch="feature/fix")
box.git.checkout(branch="release/v2")
pr = box.git.create_pr(title="Fix bug", body="...", base="main")
# pr: PullRequest(url, number, title, base)
# Update the box-wide git identity
cfg = box.git.update_config(user_name="Bot", user_email="bot@example.com")
# cfg: GitConfigResult(git_user_name, git_user_email)
# Arbitrary git commands — returns the output string only. Unlike the JS SDK, which
# returns { output, exit_code }, Python drops the status, so a failure (exit 128 when
# the cwd is not a repository) is indistinguishable from success — check the output.
output = box.git.exec(args=["log", "--oneline", "-5"])
```
## Schedules
Cron tasks on a box — shell commands or agent prompts. Available on `Box` and `EphemeralBox`. Cron is UTC.
```python
exec_schedule = box.schedule.exec(
cron="* * * * *",
command=["bash", "-c", "date >> /workspace/home/cron.log"],
folder="/workspace/home", # optional cwd override
webhook_url="https://example.com/hook",
webhook_headers={"Authorization": "Bearer ..."},
)
agent_schedule = box.schedule.agent(
cron="0 9 * * *",
prompt="Run the test suite and fix any failures",
folder="/workspace/home/repo", # optional cwd override
model="anthropic/claude-sonnet-5", # optional override
options={"max_budget_usd": 1.0, "effort": "high"},
timeout=300_000,
webhook_url="https://example.com/hook",
webhook_headers={"Authorization": "Bearer ..."},
)
schedules = box.schedule.list()
one = box.schedule.get(agent_schedule.id)
# Partial update — omitted args keep their value, "" / [] / {} clear a field,
# options=None clears agent options. The schedule's type cannot change.
# Updatable: cron, command, prompt, folder, model, options, timeout,
# webhook_url, webhook_headers
box.schedule.update(agent_schedule.id, cron="0 18 * * *", webhook_url="")
box.schedule.pause(agent_schedule.id)
box.schedule.resume(agent_schedule.id)
box.schedule.delete(agent_schedule.id)
```
## Snapshots
```python
# Snapshot — checkpoint workspace state
snap = box.snapshot(name="after-setup")
# snap: Snapshot(id, name, box_id, size_bytes, status, created_at)
# from_snapshot takes the same BoxConfig kwargs as create (shared request body):
# name, labels, size, keep_alive, init_command, runtime, browser, agent, git, env,
# attach_headers, network_policy, skills, mcp_servers. Note the JS SDK's
# Box.fromSnapshot() drops browser / skills / mcpServers — Python forwards them.
restored = Box.from_snapshot(
snap.id,
size="medium",
keep_alive=True,
# the git identity is forwarded, not just the token
git={"token": os.environ["GITHUB_TOKEN"], "user_name": "Bot", "user_email": "bot@example.com"},
env={"DATABASE_URL": "..."},
)
snaps = box.list_snapshots()
box.delete_snapshot(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.
```python
from pydantic import BaseModel
box = Box.create(browser=True, agent={"harness": Agent.CLAUDE_CODE, "model": ClaudeCode.SONNET_4_5})
# Tabs
tab = box.browser.tab.create("https://example.com", wait_until="load", timeout=30_000)
tabs = box.browser.list_tabs()
again = box.browser.get_tab(tab.id) # no network call
tab.id, tab.url, tab.title # handle metadata, no network call
# Page operations
content = tab.goto("https://news.ycombinator.com") # BrowserContent(title, url, text, links)
current = tab.content()
png = tab.screenshot() # bytes
b64 = tab.screenshot(encoding="base64", full_page=True)
# AI operations (metered) — schema is a Pydantic model or a raw JSON-schema dict.
# extract / observe / act take an optional model= override, defaulting to the box's
# model (or anthropic/claude-sonnet-4-5 when it has none).
class Story(BaseModel):
title: str
points: int
data = tab.extract("Top story title and points", Story, model="anthropic/claude-sonnet-4-5")
# observe → actionable elements, each carrying a replayable method + arguments
elements = tab.observe("What can I click?", model="openai/gpt-5.6").elements
# elements: [BrowserObserveElement(description, selector, url, method, arguments)]
acted = tab.act("Click the first headline")
# BrowserActResult(success, message, action_description, actions, cache_status,
# input_tokens, output_tokens)
# Replay a pre-resolved action — no LLM call, no tokens, no model provider key.
# Pass a BrowserObserveElement or BrowserActAction instead of a string; `model` is
# ignored in this form, and an action without a `selector` raises BoxError.
tab.act(elements[0])
tab.act(acted.actions[0])
# Live view + raw CDP
live_url = tab.live_view_url() # view-only screencast page/iframe
cdp_url = box.browser.cdp_url() # wss://…?token=… — no extra auth wiring
tab.close()
# Drive the same browser from Playwright (pip install playwright)
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
remote = p.chromium.connect_over_cdp(cdp_url)
context = remote.contexts[0] if remote.contexts else remote.new_context()
page = context.pages[0] if context.pages else context.new_page()
page.goto("https://example.com")
# Session recordings (HLS playback URL + MP4 download, chapter markers).
# One active recording per box; captures all tabs and follows the foreground.
# Auto-stops after max_duration_seconds or ~3 minutes of no on-screen activity.
handle = box.browser.recordings.start(max_duration_seconds=600) # default & max 600
recording = handle.stop()
# or stop whatever is recording on the box, without a handle:
# recording = box.browser.recordings.stop()
# BrowserRecording(id, box_id, status, started_at, ended_at, duration_ms, size_bytes,
# mp4_size_bytes, segment_count, markers, stopped_reason,
# max_duration_seconds, expires_at, playlist_url)
# markers: BrowserRecordingMarker(type="tab_switch", at_ms, end_ms, label, tab_id)
# expires_at is epoch ms (videos retained 14 days); playlist_url is API-served — fetch it
# with an `X-Box-Api-Key: <api_key>` header (hls.js / Safari / ffplay).
all_recordings = box.browser.recordings.list()
one_recording = box.browser.recordings.get(recording.id)
# Download the video to a local file — returns the path written.
# Defaults to ./box-recording-<id>.mp4 (.ts for recordings captured before MP4 support).
file = box.browser.recordings.download(recording.id)
box.browser.recordings.download(recording.id, path="./out/demo.mp4")
```
### Multi-step browser goals
`tab.run()` — the autonomous multi-step browser agent — was **removed**, along with
the `BrowserRunResult` / `BrowserRunStep` types (Stagehand v4 dropped the underlying
agent primitive). The 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.
```python
class Product(BaseModel):
title: str
price: str
elements = tab.observe("the product links in the listing").elements
actions = [e for e in elements if e.selector]
for action in actions[:5]:
tab.goto(START) # deterministic reset, no browser-AI tokens
tab.act(action) # replay the resolved click: no LLM, no tokens
item = tab.extract("title and price", Product)
```
**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 via `box.browser.cdp_url()` 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.
```python
from upstash_box import EphemeralBox
ebox = EphemeralBox.create(
name="scratch-box",
runtime="python",
size="small",
ttl=3600, # seconds, max 259200 (3 days), default 259200
env={"API_KEY": "..."},
labels=["scratch"], # settable at create time; filter via Box.list(label=...)
network_policy={"mode": "deny-all"},
attach_headers={"api.stripe.com": {"Authorization": "Bearer sk_live_..."}},
)
ebox.network_policy
ebox.expires_at # unix timestamp when auto-deleted
ebox.exec.command("python -c 'print(1+1)'")
ebox.exec.code(code="print('hi')", lang="python")
ebox.exec.session(argv=["bash", "-i"], tty=True) # whole exec namespace, session included
ebox.files.write(path="/workspace/home/data.json", content="{}")
ebox.files.stat("/workspace/home/data.json") # whole files namespace, stat/mkdir/rename/remove included
ebox.schedule.exec(cron="* * * * *", command=["bash", "-c", "date"])
ebox.cd("subdir")
snap = ebox.snapshot(name="checkpoint")
ebox.list_snapshots()
ebox.delete_snapshot(snap.id)
status = ebox.get_status()["status"]
ebox.delete()
# Restore from snapshot
ebox2 = EphemeralBox.from_snapshot(snap.id, ttl=7200)
# Statics: EphemeralBox.delete_boxes(box_ids=[...]) / EphemeralBox.delete_snapshots(...)
# are the Box ones. EphemeralBox.get_by_name() returns a full `Box`, not an
# `EphemeralBox` (quirk mirrored from the JS SDK).
# `AsyncEphemeralBox` is the async variant (`await AsyncEphemeralBox.create(...)`).
```
## Public URLs
Expose box ports as public URLs with optional auth.
```python
public_url = box.get_public_url(3000)
# public_url: PublicURL(url="https://{id}-3000.preview.box.upstash.com", port)
authed = box.get_public_url(3000, bearer_token=True)
# authed: PublicURL(url, port, token)
basic = box.get_public_url(3000, basic_auth=True)
# basic: PublicURL(url, port, username, password)
result = box.list_public_urls() # {"public_urls": [PublicURL, ...]}
box.delete_public_url(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.
```python
box = Box.create(skills=["upstash/qstash-js/qstash-js"])
box.skills.add("upstash/workflow-js/workflow-js")
enabled = box.skills.list()
box.skills.remove("upstash/workflow-js/workflow-js")
```
## Labels
```python
labels = box.labels.add("prod") # returns the updated set
box.labels.remove("beta")
current = box.labels.list()
prod_boxes = Box.list(label="prod")
```
## Network Policy & Outbound Headers
```python
box = Box.create(
# mode: "allow-all" (default) | "deny-all" | "custom"
# custom takes any of allowed_domains / allowed_cidrs / denied_cidrs
network_policy={
"mode": "custom",
"allowed_domains": ["api.example.com"],
"allowed_cidrs": ["203.0.113.0/24"],
"denied_cidrs": ["10.0.0.0/8"],
},
# Inject secret headers into matching outbound HTTPS requests (write-only, never read back)
attach_headers={
"api.stripe.com": {"Authorization": "Bearer sk_live_..."},
"*.example.com": {"X-Custom-Token": "secret123"},
},
)
box.network_policy
box.update_network_policy({"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 `network_policy` restrictive when the agent
also handles untrusted input.
```python
box = Box.create(
agent={"harness": Agent.CLAUDE_CODE, "model": ClaudeCode.SONNET_4_5},
mcp_servers=[
{"name": "fs", "package": "@modelcontextprotocol/server-filesystem"},
{"name": "custom", "url": "<your-mcp-server-url>", "headers": {"Authorization": "..."}},
],
)
```
## Errors & SSH
```python
from upstash_box import BoxError
try:
box.agent.run(prompt="...")
except BoxError as e:
print(e, e.status_code)
```
Shell into a box directly (Box API key is the SSH password):
```bash
ssh <box-id>@us-east-1.box.upstash.com
```
## Async client
The async client mirrors the sync API exactly — `await` the calls and use `async for` to stream.
```python
import asyncio
from upstash_box import AsyncBox, Agent
async def main():
box = await AsyncBox.create(runtime="node", agent={"harness": Agent.CLAUDE_CODE})
async with box:
run = await box.agent.run(prompt="Set up a Next.js project")
print(run.result)
stream = await box.agent.stream(prompt="Build a REST API")
async for chunk in stream:
print(chunk)
await box.delete()
asyncio.run(main())
```
`asyncio.gather` over many `AsyncBox.create(...)` / `box.agent.run(...)` calls runs boxes in parallel.
## Gotchas
- Public API option keys are **snake_case** in Python: `api_key`, `user_name`, `network_policy`, `response_schema`, `max_retries`, `on_tool_use`, `attach_headers`, and agent `options` like `max_turns`, `max_budget_usd`.
- Agent config takes **`harness`** (not the deprecated `provider`/`runner`) — `harness` is required.
- `response_schema` accepts a Pydantic `BaseModel` subclass (returns a typed instance) or a raw JSON-schema `dict` (returns a `dict`). Browser `schema` follows the same contract.
- 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.
- `EphemeralBox` does NOT support `agent`, `git`, `skills`, the `labels` namespace, the browser, or public URLs — use full `Box` for those (it does support `schedule` and snapshots).
- `run.exit_code` is `None` 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.
- `files.read()` slices only when `length` is given — `offset=` alone reads the whole file, and `length=0` reads nothing.
- `files.stat()` is an lstat by default: a symlink reports `type="symlink"` unless you pass `follow=True`.
- `files.remove()` needs `recursive=True` for a directory, and `files.mkdir()` needs `parents=True` for nested paths.
- `files.rename(from_path, to_path)` takes positional arguments (`from` is a Python keyword); JS spells it `rename(from, to)`.
- `exec.session()` handles own the process — `close()` or a dropped connection kills the command, and sessions cannot be reattached. `tty=True` merges stderr into stdout, so `on_stderr` never fires for a PTY session.
- The sync `session.wait(timeout=...)` has no async counterpart (`await handle.wait()` blocks until exit); it raises `TimeoutError` when the timeout elapses.
- `box.browser` requires a box created with `browser=True`.
- There is **no** `tab.run()` — the autonomous browser agent was removed. Loop `observe` + `act(action)` + `extract` yourself, hand the goal to the in-box agent, or drive Playwright over `cdp_url()`.
- `tab.act(action)` (replaying an `observe()` result) costs no tokens and needs no model provider key; only `act(instruction)` with a string is metered.
- `get_init_command` / `set_init_command` / `delete_init_command` raise unless the box was created with `keep_alive=True`.
- The JS static `Box.delete({boxIds})` is `Box.delete_boxes(box_ids=...)` here, to avoid clashing with the instance `delete()`.
- `box.delete()` is irreversible — snapshot first if you need the state.
- Git operations require `git.token` in the box config for private repos and PRs.
- `Box.from_snapshot()` creates a new box — it does not modify the original. It reuses the full create body, so `browser` / `skills` / `mcp_servers` are forwarded (the JS `Box.fromSnapshot()` drops those).
- `EphemeralBox` has no `update_network_policy` — set `network_policy` at create time.
- All `timeout` values are in **milliseconds** (matching the JS SDK), default `600000`.
- When breaking out of a stream early, call `stream.close()` / `await stream.aclose()` so the run is marked `detached`.
- Close the transport when done: `box.delete()` closes it, or use `with box:` / `box.close()` (`async with` / `await box.aclose()` for `AsyncBox`).
upstash-box-remote-work8.37 KB
---
name: upstash-box-remote-work
description: Do work in an Upstash Box, a sandboxed cloud container driven through the remote Upstash MCP server (mcp.upstash.com), instead of on the local machine. Use when the user asks to run, build, test, clone, or edit something remotely, in a sandbox, in the cloud, or in a box, when the deliverable is a pull request, a public preview URL, or a screenshot of a running app, when the local machine cannot deliver (no GitHub login for gh, no way to expose a port, a dirty or slow local checkout), or when several independent tasks should run in parallel on separate machines, a code factory that turns a list of tasks into a list of pull requests. Applies whenever the session has the box_* and blob_* MCP tools, even when Upstash is not named.
---
A box is a sandboxed Linux container in the cloud with a shell, a filesystem,
git, an optional headless Chromium, and public URLs for its ports. Everything
here goes through the remote Upstash MCP server. There is no SDK to install
and no API key in the environment: the server forwards the session's OAuth
token to the Box API, and screenshot bytes travel from the box to Blob
without passing through the server.
## When to take work into a box
- The user asks for it: remote, in a sandbox, in the cloud, in a box, not on
my machine.
- The deliverable is a **pull request**, a **public preview URL**, or a
**screenshot** of the running result. A box has GitHub credentials, public
ports and a browser; the local machine often has none of the three.
- The work needs isolation: untrusted or generated code, a heavy dependency
install, a clean checkout, branches the local tree should not carry.
- The work scales out: several independent tasks, one box each, in parallel.
Once a task is in a box, do all of it there. The box's filesystem is not the
local one, so an edit made locally and a build run in the box act on two
different checkouts, and neither side reports the mismatch.
## Connect
The plugin already registers `https://mcp.upstash.com/mcp`, and the box and
blob tools are part of its default tool set. To add the server by hand:
```bash
claude mcp add --scope user --transport http upstash "https://mcp.upstash.com/mcp"
```
On first use the client opens the Upstash consent page. Pick the account or
team the boxes and buckets should live in, and **turn the read-only switch
off**: every step below except listing is refused with 403 on a read-only
grant.
## Tools
| Tool | Actions / purpose |
|---|---|
| `box_manage` | create, list, get, delete, pause, resume, fork |
| `box_exec` | run a shell command in the box (`command` is an argv array, `folder` is the working directory) |
| `box_git` | clone, status, diff, commit, checkout, push, create_pr |
| `box_preview` | create, list, delete public URLs for ports in the box |
| `box_browser` | goto, content, screenshot, tabs, tab_new, tab_close, live_view |
| `box_snapshots` | create, list, list_all, delete, restore (a new box from a snapshot) |
| `box_logs`, `box_runs` | what happened inside a box, and its run history |
| `box_apikey` | list, create, delete Box API keys for a deployed app or CI (the key outlives the OAuth grant, so tell the user to revoke it when done) |
| `blob_bucket` | list, create (create defaults to `visibility: public`) |
| `blob_upload_url` | presigned PUT URLs for paths in a bucket, plus `public_url` on public buckets |
## The flow: one task, one box
1. **Create.** `box_manage` `create`. Set `browser: true` if you will take
screenshots or check pages. Use `ephemeral: true` with a `ttl` for
throwaway work (no paid plan needed); use `keep_alive: true` when a preview
URL must outlive the session (paid plan). Note the returned `id`.
2. **Clone with `box_git` `clone`**, never with `git` in `box_exec`. The clone
is what writes the account's GitHub credentials into the box; without it
`push` and `create_pr` fail with a bare 500. The checkout lands at
`/workspace/home/<repo name>`. Pass that as `folder` on every later
`box_exec` and `box_git` call: the default is the workspace root, which is
not a repository.
3. **Work.** `box_exec` for install, build, tests, and the app itself. Each
call waits for the command, so detach servers:
`["sh", "-c", "( pnpm preview --host 0.0.0.0 --port 4321 > /workspace/home/app.log 2>&1 & )"]`,
then poll the port with `curl` in a second call. Edit files with shell
commands or a short script in `box_exec`, not with local file tools.
4. **Preview URL.** `box_preview` `create` with the `port`. The app must
listen on `0.0.0.0`; a server bound to `127.0.0.1` answers curl inside the
box and still gives 502 through the preview. The URL has the shape
`https://<box-id>-<port>.preview.box.upstash.com`. Add `basic_auth` or
`bearer_token` when the page should not be open to anyone with the link;
the credential is returned once. Say in the reply how long the URL lives:
an ephemeral box takes it down at `expires_at`.
5. **Screenshots.** `box_browser` `goto` the page on `http://localhost:<port>`,
then `screenshot`. With no `path` the PNG comes back as an image you can
look at; with a `path` it is written inside the box and only `{saved,
bytes}` returns. Look while you work, save the ones that count, and pass
`full_page: true` for long pages.
6. **Publish screenshots.** GitHub's attachment endpoint rejects the box's
token, so images go through Blob. `blob_bucket` `list`, and `create` a
**public** one if none fits (private buckets only serve short-lived
signed URLs). `blob_upload_url` with `bucket_id` and
`files: [{path: "<repo>/<branch>/after.png", content_type: "image/png", size: <bytes>}]`,
minted right before use (a URL lives at most 10 minutes). Then `box_exec`
the `curl_example` from the result, sending the returned headers verbatim
(they are part of the signature). Bytes go straight from the box to storage.
7. **Pull request.** `box_git` `checkout` a branch, `commit`, `push` with the
branch name, then `create_pr` with `base`, `title`, and a `body` that
carries the preview URL and `` for each screenshot.
`create_pr` pushes nothing itself. Reply with the PR URL, the preview URL,
and the screenshot URLs.
8. **Clean up.** `box_manage` `delete` (or `pause`) unless the user wants the
box or its preview kept. Ephemeral boxes expire on their own.
## Scaling out
- One box per independent task. Create them with a shared `labels` entry,
drive them in parallel, and `box_manage` `list` with `label` to find and
delete them at the end. Never run two tasks in one box at once.
- When every task needs the same expensive setup (clone, dependency install,
build cache), do it once, `box_snapshots` `create`, then `restore` one new
box per task from the snapshot. `fork` does the same for an idle or paused
non-ephemeral box.
- `size` is `small`, `medium` or `large`; pick it per task rather than
oversizing all of them.
- A `live_view` URL lets a person watch a box's browser tab as it works
(frames out, no input in); hand it over for long runs.
## Gotchas
- Read-only grant → 403 on create, exec, screenshot-to-path, git writes and
upload URLs. Re-consent with read-only off.
- `box_git` `create_pr` fails if the account has no GitHub installation
covering the repo; ask the user to connect GitHub in the Upstash console
under Box settings.
- Prefer `box_git` `clone` over `clone_repo` on `box_manage` `create`: on an
ephemeral box the option can be ignored, and on a persistent one it runs in
the background with no completion signal beyond `box_logs`.
- Paths inside the box are relative to `/workspace/home` unless absolute.
`ls /workspace` itself is denied (root-owned, mode 711).
- The `node` image has no corepack and boxuser cannot `npm i -g`, so a
`packageManager`-pinned pnpm fails to self-install. Use
`npx -y pnpm@<version>` or `export npm_config_manage_package_manager_versions=false`
(the preinstalled pnpm); `sudo npm i -g pnpm@<version>` also works, sudo is
passwordless.
- `blob_upload_url` headers are signed. A missing or changed `content-type`
or `cache-control` → 403 from storage (`SignatureDoesNotMatch`). An expired
URL → mint again.
- Bucket names are account-wide. Reuse one bucket such as `agent-proof` with
per-repo prefixes rather than creating one per run.
- `box_browser` fails with "browser is not enabled for this box" unless the
box was created with `browser: true`; there is no way to add it later.
upstash-cli8.81 KB
---
name: upstash-cli
description: Run the Upstash CLI (`upstash`) against the Upstash Developer API for Redis, Vector, Search, QStash, Blob, and teams, with non-interactive commands and JSON output for scripts, CI, and agents. Use when creating, listing, renaming, or deleting Redis databases, changing plans, regions, TLS, eviction, auto-upgrade, or budgets, managing backups, running Redis commands with `upstash redis exec`, creating or inspecting Vector and Search indexes, managing QStash instances and tokens, creating Blob buckets or minting temporary S3 credentials for one, managing team members, reading usage stats, or automating any Upstash account operation from the terminal. Also use when the user asks how to provision or manage Upstash resources without the console. Prefer the Upstash MCP server when its tools are available in the session, and use this skill for terminal, CI, and scripting work.
license: MIT
metadata:
author: Upstash
homepage: https://upstash.com
---
## Prefer the MCP server when it is available
If Upstash MCP tools are in the session, call them instead of shelling out to the CLI. They are
already authenticated and cover the same ground — creating and inspecting Redis databases, running
Redis commands, usage stats, backups, Vector and Search indexes, Blob buckets, QStash schedules and
messages, the DLQ, and logs. Installing the Upstash plugin registers the hosted server at
`https://mcp.upstash.com/mcp`.
Use the CLI when there is no MCP in the session, or when the work is inherently shell work — a CI
step, a provisioning script, or piping JSON into other commands.
The Upstash CLI (`upstash`) manages Upstash services via the Upstash Developer API. All commands are non-interactive and emit JSON on stdout. Errors go to stderr as `{ "error": "..." }` with exit code `1`.
## Install
```bash
npm i -g @upstash/cli
```
## Authentication
Recommended: run `upstash login` once per machine. Prompts for email and a Developer API key (create one at https://console.upstash.com/account/api), verifies them, and saves to `~/.config/upstash/config.json`.
```bash
upstash login
```
Alternatives — env vars (also auto-loaded from a `.env` in cwd), or `--email` / `--api-key` inline, or `--env-path <path>` to point at a specific `.env`:
```bash
export UPSTASH_EMAIL=you@example.com
export UPSTASH_API_KEY=your_api_key
```
Precedence: flags > env vars > `.env` > saved config. Prefer a **read-only** API key for agents when possible — mutations fail at the API, the same way they would in the console.
## Resource ID flags
| Flag | Products |
|------|----------|
| `--db-id <id>` | Redis |
| `--index-id <id>` | Vector, Search |
| `--qstash-id <id>` | QStash |
| `--bucket-id <id>` | Blob |
| `--team-id <id>` | Team |
## Redis
```bash
upstash redis list
upstash redis get --db-id <id> [--hide-credentials]
upstash redis create --name <name> --region <region> [--read-regions <r1> <r2>]
upstash redis delete --db-id <id> [--dry-run]
upstash redis rename --db-id <id> --name <new-name>
upstash redis reset-password --db-id <id>
upstash redis stats --db-id <id>
upstash redis enable-tls --db-id <id>
upstash redis {enable,disable}-eviction --db-id <id>
upstash redis {enable,disable}-autoupgrade --db-id <id>
upstash redis change-plan --db-id <id> --plan <free|payg|pro|paid>
upstash redis update-budget --db-id <id> --budget <cents>
upstash redis update-regions --db-id <id> --read-regions <r1> <r2>
upstash redis move-to-team --db-id <id> --team-id <id>
```
Regions — AWS: `us-east-1`, `us-east-2`, `us-west-1`, `us-west-2`, `ca-central-1`, `eu-central-1`, `eu-west-1`, `eu-west-2`, `sa-east-1`, `ap-south-1`, `ap-northeast-1`, `ap-southeast-1`, `ap-southeast-2`, `af-south-1`. GCP: `us-central1`, `us-east4`, `europe-west1`, `asia-northeast1`.
### Redis backups
```bash
upstash redis backup list --db-id <id>
upstash redis backup create --db-id <id> --name <name>
upstash redis backup delete --db-id <id> --backup-id <id> [--dry-run]
upstash redis backup restore --db-id <id> --backup-id <id>
upstash redis backup {enable,disable}-daily --db-id <id>
```
### Redis exec (REST, not the Developer API key)
```bash
upstash redis exec --db-url <url> --db-token <token> SET key value
upstash redis exec --db-url <url> --db-token <token> --json '["SET","key","value"]'
```
`--db-url` / `--db-token` fall back to `UPSTASH_REDIS_REST_URL` / `UPSTASH_REDIS_REST_TOKEN`. Get both from `endpoint` and `rest_token` in `upstash redis get --db-id <id>`.
## Team
```bash
upstash team list
upstash team create --name <name> [--copy-cc]
upstash team delete --team-id <id> [--dry-run]
upstash team members --team-id <id>
upstash team add-member --team-id <id> --member-email <email> --role <admin|dev|finance>
upstash team remove-member --team-id <id> --member-email <email> [--dry-run]
```
## Vector
```bash
upstash vector list
upstash vector get --index-id <id>
upstash vector create --name <name> --region <region> --similarity-function <fn> --dimension-count <n> [--type payg] [--index-type <type>] [--embedding-model <m>] [--sparse-embedding-model <m>]
upstash vector delete --index-id <id> [--dry-run]
upstash vector rename --index-id <id> --name <new-name>
upstash vector reset-password --index-id <id>
upstash vector set-plan --index-id <id> --plan <free|payg|fixed>
upstash vector transfer --index-id <id> --target-account <id>
upstash vector stats # aggregate across all indexes
upstash vector index-stats --index-id <id> [--period <1h|3h|12h|1d|3d|7d|30d>]
```
Regions: `eu-west-1`, `us-east-1`, `us-central1`. Similarity: `COSINE`, `EUCLIDEAN`, `DOT_PRODUCT`. Index types: `DENSE`, `SPARSE`, `HYBRID`. Dense models: `BGE_SMALL_EN_V1_5`, `BGE_BASE_EN_V1_5`, `BGE_LARGE_EN_V1_5`, `BGE_M3`. Sparse models: `BM25`, `BGE_M3`. For `HYBRID` with managed embeddings, set `--dimension-count 0`.
## Search
```bash
upstash search list
upstash search get --index-id <id>
upstash search create --name <name> --region <region> --type <free|payg|fixed>
upstash search delete --index-id <id> [--dry-run]
upstash search rename --index-id <id> --name <new-name>
upstash search reset-password --index-id <id>
upstash search transfer --index-id <id> --target-account <id>
upstash search stats
upstash search index-stats --index-id <id> [--period <1h|3h|12h|1d|3d|7d|30d>]
```
Regions: `eu-west-1`, `us-central1`.
## QStash
```bash
upstash qstash list # run first; maps region → id
upstash qstash get --qstash-id <id>
upstash qstash rotate-token --qstash-id <id>
upstash qstash set-plan --qstash-id <id> --plan <paid|qstash_fixed_1m|qstash_fixed_10m|qstash_fixed_100m>
upstash qstash stats --qstash-id <id> [--period <1h|3h|12h|1d|3d|7d|30d>]
upstash qstash ipv4 # CIDR blocks for allowlisting
upstash qstash move-to-team --qstash-id <id> --target-team-id <id>
upstash qstash update-budget --qstash-id <id> --budget <dollars> # 0 = no limit
upstash qstash {enable,disable}-prodpack --qstash-id <id>
```
## Blob
Run `upstash blob --help` to check that the installed CLI includes Blob support. If the command is unavailable, use the console for bucket management and `upstash-blob-js` for application code until a CLI release includes it.
```bash
upstash blob list
upstash blob get --bucket-id <id> [--hide-credentials]
upstash blob create --name <name> [--visibility <private|public>] [--cors <origin> <origin>]
upstash blob delete --bucket-id <id> [--dry-run]
upstash blob credentials [--bucket-id <id>]
```
Buckets are `private` by default. `blob get` returns the bucket token, which is a bearer secret for the whole bucket — pass `--hide-credentials` when you only need the metadata.
`blob credentials` exchanges a bucket token for temporary, bucket-scoped S3 credentials to use with the AWS CLI, rclone, or any S3 SDK. With `--bucket-id` it reads the token through the Developer API; with no flag it uses `UPSTASH_BLOB_TOKEN` and needs no account auth at all. `expiresAt` is the expiry as a **unix timestamp in seconds** (multiply by 1000 before comparing with `Date.now()`) — re-mint before it passes rather than caching.
```bash
CREDS=$(upstash blob credentials --bucket-id <id>)
export AWS_ACCESS_KEY_ID=$(echo "$CREDS" | jq -r .accessKeyId)
export AWS_SECRET_ACCESS_KEY=$(echo "$CREDS" | jq -r .secretAccessKey)
export AWS_SESSION_TOKEN=$(echo "$CREDS" | jq -r .sessionToken)
aws s3 cp image.png "s3://$(echo "$CREDS" | jq -r .bucket)/image.png" \
--endpoint-url "$(echo "$CREDS" | jq -r .endpoint)" --region auto
```
Object operations are not CLI commands — the bucket is S3-compatible, so use the credentials above with an S3 tool, or the `upstash-blob-js` skill for the TypeScript SDK.
## Conventions
- Pipe any output to `jq` for field extraction, e.g. `upstash redis list | jq '.[].database_id'`.
- Use `--dry-run` first on any `delete` or `remove-member`.
- Use `--hide-credentials` on `redis get` and `blob get` when the secret isn't needed.
upstash-qstash-js3.76 KB
---
name: upstash-qstash-js
description: Work with the @upstash/qstash TypeScript/JavaScript SDK, an HTTP-based message queue, task scheduler, and background job system for serverless and edge runtimes (Next.js, Vercel, Cloudflare Workers, Deno, Node.js). Use when publishing messages to HTTP endpoints or URL groups, running background jobs without a long-running worker process, scheduling with cron expressions, delaying messages, building FIFO queues with parallelism and flow control, configuring retries and callbacks, handling a dead letter queue (DLQ), deduplicating messages, fanning out to multiple endpoints, verifying QStash webhook signatures (Next.js App Router, Pages Router, and Edge Runtime), running a local QStash dev server, or migrating regions. Also use when the user asks for a serverless cron job, async task queue, job scheduler, delayed delivery, webhook delivery with retries, or event-driven messaging between services.
license: MIT
metadata:
author: Upstash
homepage: https://upstash.com
---
# QStash JavaScript SDK
QStash is an HTTP-based messaging and scheduling solution for serverless and edge runtimes. This skill helps you use the QStash JS SDK effectively.
## When to use this skill
Use this skill when:
- Publishing HTTP messages to endpoints or URL groups
- Creating scheduled or delayed message delivery
- Managing FIFO queues with configurable parallelism
- Verifying incoming webhook signatures from QStash
- Implementing callbacks, DLQ handling, or message deduplication
## Quick Start
### Installing the SDK
```bash
npm install @upstash/qstash
```
### Basic Publishing
```typescript
import { Client } from "@upstash/qstash";
const client = new Client({
token: process.env.QSTASH_TOKEN!,
});
const result = await client.publishJSON({
url: "https://my-api.example.com/webhook",
body: { event: "user.created", userId: "123" },
});
```
## Core Concepts
For fundamental QStash operations, see:
- [Publishing Messages](fundamentals/publishing-messages.md)
- [Schedules](fundamentals/schedules.md)
- [Queues and Flow Control](fundamentals/queues-and-flow-control.md)
- [URL Groups](fundamentals/url-groups.md)
- [Local Development](fundamentals/local-development.md) — automatic dev server via `devMode: true`
For verifying incoming messages:
- [Receiver Verification](verification/receiver.md) - Core signature verification with the Receiver class
- Platform-Specific Verifiers:
- [Next.js](verification/platform-specific/nextjs.md) - App Router, Pages Router, and Edge Runtime
For advanced features:
- [Callbacks](advanced/callbacks.md)
- [Dead Letter Queue (DLQ)](advanced/dlq.md)
- [Message Deduplication](advanced/deduplication.md)
- [Region migration & multi-region support](advanced/multi-region/summary.md)
- If needed, [multi-region env variable setup verification script](advanced/multi-region/verify-multi-region-setup.ts). Can be run without arguments
## Platform Support
QStash JS SDK works across various platforms:
- Next.js (App Router and Pages Router)
- Cloudflare Workers
- Deno
- Node.js (v18+)
- Vercel Edge Runtime
- SvelteKit, Nuxt, SolidJS, and other frameworks
> **Note on Workflow SDK:** For building complex durable workflows that chain multiple QStash messages together, consider using the separate QStash Workflow SDK (`@upstash/workflow`). The Workflow SDK empowers you to orchestrate multi-step processes with automatic state management, retries, and fault tolerance. This Skills file focuses on the core QStash messaging SDK.
## Best Practices
- Always verify incoming QStash messages using the Receiver class
- Use environment variables for tokens and signing keys
- Set appropriate retry counts and timeouts for your use case
- Use queues for ordered processing with controlled parallelism
- Implement DLQ handling for failed message recovery
Referenced files: 12
upstash-ratelimit-js1.87 KB
---
name: upstash-ratelimit-js
description: Rate limiting for serverless and edge apps with the @upstash/ratelimit TypeScript/JavaScript SDK backed by Upstash Redis. Use when adding a rate limiter or throttling to an API route, Next.js middleware, Vercel Edge, Cloudflare Workers, or any HTTP endpoint; returning 429 Too Many Requests; choosing between fixed window, sliding window, and token bucket algorithms; limiting per user, IP, API key, or tenant with prefixes and custom keys; protecting login, signup, form, or AI endpoints from abuse, bots, and brute force; using deny lists, ephemeral caching, analytics, timeouts, and multi-region rate limits; or estimating the Redis command cost of rate limiting. Also use when the user says rate limit, rate-limiting, throttle, quota, request limits, or traffic protection.
license: MIT
metadata:
author: Upstash
homepage: https://upstash.com
---
# Rate Limit TS SDK
## Quick Start
- Install the SDK and connect to Redis.
- Create a rate limiter and apply it to incoming operations.
Example:
```ts
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";
const redis = new Redis({ url: "<url>", token: "<token>" });
const limiter = new Ratelimit({ redis, limiter: Ratelimit.slidingWindow(5, "10s") });
const { success } = await limiter.limit("user-id");
if (!success) {
// throttled
}
```
## Other Skill Files
- **algorithms.md**: Describes all available rate‑limiting algorithms and how they behave.
- **pricing-cost.md**: Explains pricing, Redis cost implications, and operational considerations.
- **features.md**: Lists SDK features such as prefixes, custom keys, and behavioral options.
- **methods-getting-started.md**: Full method reference for the SDK's API and getting started guide.
- **traffic-protection.md**: Guidance on applying rate limiting for traffic shaping, abuse prevention, and protection patterns.
Referenced files: 5
upstash-redis-js7.32 KB
---
name: upstash-redis-js
description: Work with the @upstash/redis TypeScript/JavaScript SDK, a serverless HTTP-based Redis client for Next.js, Vercel, Cloudflare Workers, edge runtimes, and Node.js. Use when adding a cache (cache-aside, write-through, TTL and expiration strategies), session storage and user sessions, a key-value store, leaderboards and rankings with sorted sets, counters, distributed locks, queues with lists, streams and consumer groups, JSON documents, pipelines and MULTI/EXEC transactions, Lua scripting, read replicas, or full-text search, typo-tolerant search, facets, and aggregations with Upstash Redis Search (different from regular FT.SEARCH; also available for TCP clients via @upstash/search-redis and @upstash/search-ioredis). Also use when migrating from ioredis or node-redis, when a Redis connection is needed from a serverless function without connection pooling, when integrating @upstash/ratelimit, or when the user says Redis cache, KV store, session store, serverless Redis, or Upstash Redis. Supports automatic serialization/deserialization of JavaScript types.
license: MIT
metadata:
author: Upstash
homepage: https://upstash.com
---
# Upstash Redis SDK - Complete Skills Guide
This directory contains comprehensive guides for using the `@upstash/redis` SDK. These skill files are designed to help developers and AI assistants understand and use the SDK effectively.
## Installation
```bash
npm install @upstash/redis
```
## Quick Start
### Basic Initialization
```typescript
import { Redis } from "@upstash/redis";
// Initialize with explicit credentials
const redis = new Redis({
url: "UPSTASH_REDIS_REST_URL",
token: "UPSTASH_REDIS_REST_TOKEN",
});
// Or initialize from environment variables
const redis = Redis.fromEnv();
```
### Environment Variables
Set these in your `.env` file:
```bash
UPSTASH_REDIS_REST_URL=https://your-redis.upstash.io
UPSTASH_REDIS_REST_TOKEN=your-token-here
```
> **No credentials yet?** If you're an agent that needs scratch Redis and the user hasn't provided a database, you can mint a temporary one with a single POST to `https://upstash.com/start-redis` (3-day TTL, no signup).
## Skill Files Overview
### Data Structures (data-structures/)
Redis data types with auto-serialization examples:
- **strings.md** - GET, SET, INCR, DECR, APPEND with automatic type handling
- **hashes.md** - HSET, HGET, HMGET with object serialization
- **lists.md** - LPUSH, RPUSH, LRANGE with array handling
- **sets.md** - SADD, SMEMBERS, set operations
- **sorted-sets.md** - ZADD, ZRANGE, ZRANK, leaderboard patterns
- **json.md** - JSON.SET, JSON.GET, JSONPath queries for nested objects
- **streams.md** - XADD, XREAD, XGROUP, consumer groups
### Advanced Features (advanced-features/)
Complex operations and optimizations:
- **auto-pipeline.md** - Automatic request batching, performance optimization
- **pipeline-and-transactions.md** - Manual pipelines, MULTI/EXEC for atomic operations
- **scripting.md** - Lua scripts, EVAL, EVALSHA for server-side logic
### Patterns (patterns/)
Common use cases and architectural patterns:
- **caching.md** - Cache-aside, write-through, TTL strategies
- **rate-limiting.md** - Integration with @upstash/ratelimit package
- **session-management.md** - Session storage and user state management
- **distributed-locks.md** - Lock implementations, deadlock prevention
- **leaderboard.md** - Sorted set leaderboards, real-time rankings
### Performance (performance/)
Optimization techniques and best practices:
- **batching-operations.md** - MGET, MSET, batch operations
- **pipeline-optimization.md** - When to use pipelines, performance tips
- **ttl-expiration.md** - Key expiration strategies, memory management
- **data-serialization.md** - Deep dive into auto serialization, custom serializers, edge cases
- **error-handling.md** - Error types, retry strategies, timeout handling, debugging tips
- **redis-replicas.md** - Global database setup, read replicas, read-your-writes consistency
### Search (search/)
Full-text search, filtering, and aggregation extension for Redis:
- **overview.md** - Schema definition, field types, pitfalls, package overview
- **commands/querying.md** - Query and count with filters, pagination, sorting, highlighting
- **commands/aggregating.md** - Metric aggregations ($avg, $sum, $stats), bucket aggregations ($terms, $range, $histogram, $facet)
- **commands/index-management.md** - Create, describe, drop indexes, waitIndexing
- **commands/aliases.md** - Index aliases for zero-downtime reindexing
- **adapters.md** - Using search with node-redis and ioredis via @upstash/search-redis and @upstash/search-ioredis
### Migrations (migrations/)
Migration guides from other libraries:
- **from-ioredis.md** - Migration from ioredis, key differences, serialization changes
- **from-redis-node.md** - Migration from node-redis, API differences
## Common Mistakes (Especially for LLMs)
### ❌ Mistake 1: Treating Everything as Strings
```typescript
// ❌ WRONG - Don't do this with @upstash/redis
await redis.set("count", "42"); // Stored as string "42"
const count = await redis.get("count");
const incremented = parseInt(count) + 1; // Manual parsing needed
// ✅ CORRECT - Let the SDK handle it
await redis.set("count", 42); // Stored as number
const count = await redis.get("count");
const incremented = count + 1; // Just use it
```
### ❌ Mistake 2: Manual JSON Serialization
```typescript
// ❌ WRONG - Unnecessary with @upstash/redis
await redis.set("user", JSON.stringify({ name: "Alice" }));
const user = JSON.parse(await redis.get("user"));
// ✅ CORRECT - Automatic handling
await redis.set("user", { name: "Alice" });
const user = await redis.get("user");
```
## Quick Command Reference
```typescript
// Strings
await redis.set("key", "value");
await redis.get("key");
await redis.incr("counter");
await redis.decr("counter");
// Hashes
await redis.hset("user:1", { name: "Alice", age: 30 });
await redis.hget("user:1", "name");
await redis.hgetall("user:1");
// Lists
await redis.lpush("tasks", "task1", "task2");
await redis.rpush("tasks", "task3");
await redis.lrange("tasks", 0, -1);
// Sets
await redis.sadd("tags", "javascript", "redis");
await redis.smembers("tags");
// Sorted Sets
await redis.zadd("leaderboard", { score: 100, member: "player1" });
await redis.zrange("leaderboard", 0, -1);
// JSON
await redis.json.set("user:1", "$", { name: "Alice", address: { city: "NYC" } });
await redis.json.get("user:1");
// Expiration
await redis.setex("session", 3600, { userId: "123" });
await redis.expire("key", 60);
await redis.ttl("key");
```
## Best Practices
1. **Use environment variables** for credentials, never hardcode
2. **Leverage auto-serialization** - pass native JavaScript types
3. **Use TypeScript types** for better type safety
4. **Set appropriate TTLs** to manage memory
5. **Use pipelines** for multiple operations
6. **Namespace your keys** (e.g., `user:123`, `session:abc`)
## Resources
- [Official Documentation](https://upstash.com/docs/redis)
- [GitHub Repository](https://github.com/upstash/redis-js)
- [API Reference](https://upstash.com/docs/redis/sdks/ts/overview)
- [Examples](https://github.com/upstash/redis-js/tree/main/examples)
## Getting Help
For detailed information on specific topics, refer to the individual skill files in the `skills/` directory. Each file contains comprehensive examples, use cases, and best practices for its topic.
Referenced files: 29
upstash-redis-start4.16 KB
---
name: upstash-redis-start
description: Provision a zero-config, no-signup, temporary Upstash Redis database for an AI agent with a single POST to https://upstash.com/start-redis, with no account, API key, or SDK setup required. Use when an agent needs scratch Redis right now and the user has not provided credentials, for short-term memory across tool calls, conversation history, a sub-agent work queue, ranked recall, or a quick prototype or demo. Covers idempotent creation and re-fetching credentials, calling the database through the body-style REST API or the official SDKs, and telling the user how to claim it. The database lives 3 days unless the user claims it; not for production data, PII, or secrets.
license: MIT
metadata:
author: Upstash
homepage: https://upstash.com
---
# Upstash Redis for Agents (`start-redis`)
A zero-config Redis database for AI agents — **no signup, no UI, no SDK setup**. One HTTP request returns a working endpoint + token. Databases live for **3 days** unless the user claims them.
Use this when:
- The agent needs Redis right now and the user has not given you credentials.
- You want short-term memory across tool calls, conversation history, a sub-agent work queue, or recent-first ranked recall.
This is built for quick experiments, prototypes, and demos rather than production workloads or anything tied to a real user account — the database is temporary and unauthenticated until claimed, so keep PII, secrets, and production credentials out of it.
## Create or re-fetch a database
```bash
# Generate a fresh UUIDv4 yourself, then POST it as the Idempotency-Key.
# The UUIDv4 you send becomes the database id.
curl -X POST -H "Idempotency-Key: <uuidv4>" https://upstash.com/start-redis
```
- Sending your own UUIDv4 makes the first call **retry-safe** — if the response is lost, retrying with the same UUID returns the same database instead of minting a duplicate.
- Re-POST with the same `Idempotency-Key` to **re-fetch credentials** for an existing database.
- The header is optional. Omit it to mint a new database with a server-generated id (returned in the response — re-fetch later by passing that id back as `Idempotency-Key`). Only UUIDv4 is accepted.
The response is markdown containing:
- `Database ID`, `Endpoint`, `Token`
- `Metrics` URL (JSON: uptime, commands, keys, throughput, memory, bandwidth)
- `Expires` date
- `Console URL` to share with the user (where they view usage and click **Claim** to keep the database)
- An inline quickstart for the body-style REST API
Parse these out of the markdown response and use them directly — there is no separate JSON envelope.
## Calling the database (body-style REST)
The returned endpoint speaks the Upstash Redis REST API. Prefer the **body-style** form: POST a JSON array as the body so you don't have to URL-encode the command.
```bash
# SET with TTL
curl https://<endpoint> \
-H "Authorization: Bearer <token>" \
-d '["SET","session:abc","{\"step\":2}","EX","3600"]'
# GET
curl https://<endpoint> \
-H "Authorization: Bearer <token>" \
-d '["GET","session:abc"]'
```
You can also use the official SDKs against the same endpoint + token (`@upstash/redis` for TS/JS, `upstash-redis` for Python).
## Common agent patterns
| Need | Commands |
|------|----------|
| Short-term memory across tool calls in one run | `SET key value EX <ttl>` / `GET key` |
| Conversation / turn-by-turn log | `LPUSH chat:<user> <json>` / `LRANGE chat:<user> 0 20` |
| Sub-agent work queue (producer/consumer) | `LPUSH jobs <json>` / `RPOP jobs` |
| Ranked memory (recent-first or score-first) | `ZADD memories <score> <member>` / `ZREVRANGE memories 0 9` |
## Tell the user
After provisioning, surface the **console URL** from the response to the user. Make clear that:
- The database expires in 3 days.
- They can view usage at the console URL and click **Claim** to keep it.
- This is unauthenticated scratch storage — don't put secrets or PII in it.
## Reference
- Service entry point (also serves the `GET` doc): https://upstash.com/start-redis
- Full REST API: https://upstash.com/docs/redis/features/restapi
- TS/JS SDK: https://upstash.com/docs/redis/sdks/ts
- Python SDK: https://upstash.com/docs/redis/sdks/py
upstash-search-js2.11 KB
---
name: upstash-search-js
description: Work with the @upstash/search TypeScript/JavaScript SDK, a serverless full-text and semantic search database with built-in reranking. Use when adding search to an app or site, creating a search index, upserting documents with searchable content and filterable metadata, running keyword, semantic, or hybrid search queries, reranking results, filtering with SQL-like or structured filter syntax, paginating with range, fetching or deleting documents, resetting an index, or checking index info. Also use when the user asks for site search, product, document, or knowledge-base search, or a managed search service that needs no cluster to run.
license: MIT
metadata:
author: Upstash
homepage: https://upstash.com
---
# Upstash Search Documentation
## Quick Start
Install the TS SDK:
```
npm install @upstash/search
```
Create a client and perform a simple upsert + search:
```
import { Search } from "@upstash/search";
const client = new Search({ url: process.env.UPSTASH_SEARCH_REST_URL, token: process.env.UPSTASH_SEARCH_REST_TOKEN });
const index = client.index("my-index");
await index.upsert({ id: "1", content: { text: "hello world" } });
const results = await index.search({ query: "hello" });
```
Basic steps:
- Create an index
- Insert or update documents
- Run searches or filtered queries
## Other Skill Files
### sdk-overview
Provides detailed documentation for all TypeScript SDK commands. Includes:
- delete: Deleting documents
- fetch: Retrieving a document
- info: Index info
- range: Range queries
- reset: Clearing an index
- search: Search queries
- upsert: Adding/updating documents
- getting-started: Setup steps for the SDK
### quick-start
Provides a fast, end-to-end workflow for creating a Search database, adding documents, and querying them. Covers essential concepts including:
- Creating a database and storing credentials
- Adding documents with content and metadata
- Understanding content vs metadata (searchability and filterability)
- Performing searches with optional reranking
- Filtering syntax with SQL-like or structured filters
- Common pitfalls and best practices
Referenced files: 2
upstash-vector-js2 KB
---
name: upstash-vector-js
description: Work with the @upstash/vector TypeScript/JavaScript SDK, a serverless vector database for embeddings, similarity search, semantic search, and RAG (retrieval-augmented generation). Use when upserting, querying, fetching, ranging, or deleting vectors, upserting raw text against an index with a built-in embedding model, choosing dense, sparse, or hybrid indexes, filtering by metadata, organizing data with namespaces, running resumable queries, or connecting Upstash Vector to an AI or LLM application. Also use when the user asks for a vector store, vector search, nearest-neighbor or kNN search, embeddings storage, semantic cache, recommendations or similarity features, or a hosted vector index that needs no infrastructure.
license: MIT
metadata:
author: Upstash
homepage: https://upstash.com
---
# Vector Documentation Skill
## Quick Start
Vector is a high‑performance vector database for storing, querying, and managing vector embeddings.
Basic workflow:
- Install the Vector TS SDK.
- Connect to a Vector instance.
- Upsert vectors, query them, and manage namespaces.
Example (TypeScript):
```ts
import { Index } from "@upstash/vector";
const index = new Index({
url: process.env.UPSTASH_VECTOR_REST_URL!,
token: process.env.UPSTASH_VECTOR_REST_TOKEN!,
});
await index.upsert([{ id: "1", vector: [0.1, 0.2], metadata: { tag: "example" } }]);
const results = await index.query({
vector: [0.1, 0.2],
topK: 5,
});
```
For full usage, refer to the linked skill files below.
## Other Skill Files
### TS SDK Reference
- `sdk-methods`: Explains SDK commands: delete, fetch, info, query, range, reset, resumable-query, upsert
### Features
- `features/namespaces`: Explains namespaces and dataset organization.
- `features/index-structure`: Covers hybrid and sparse index structures.
- `features/filtering-and-metadata`: Details metadata storage and server-side filtering.
Use these files for deeper guidance on SDK usage, advanced configurations, algorithms, and integrations.
Referenced files: 4
upstash-workflow-js3.09 KB
---
name: upstash-workflow-js
description: Work with the @upstash/workflow TypeScript/JavaScript SDK for durable, long-running workflows in serverless functions, multi-step processes that survive timeouts, retries, and restarts (built on QStash). Use when defining a workflow endpoint with serve(), running steps with context.run, sleeping for minutes to days without holding a function open, calling external APIs with context.call, waiting for an external event or webhook, invoking other workflows, configuring retries, failure callbacks, and a DLQ, controlling concurrency, rate, and parallelism, triggering, cancelling, or inspecting runs with the Workflow client, building AI agents and orchestrators, human-in-the-loop approvals, realtime updates, local development with the QStash dev server, adding middleware, or migrating workflows safely. Also use when the user asks for durable execution, step functions, saga or orchestration patterns, background jobs with checkpoints, or long-running tasks on Vercel, Next.js, Cloudflare Workers, or other serverless platforms.
license: MIT
metadata:
author: Upstash
homepage: https://upstash.com
---
# Upstash Workflow SDK
## Quick Start
The Upstash Workflow SDK lets you expose serverless workflow endpoints and run them reliably using QStash under the hood.
Install:
```bash
npm install @upstash/workflow
```
Define a simple workflow endpoint:
```ts
import { serve } from "@upstash/workflow";
export const { POST } = serve(async (context) => {
await context.run("step-1", () => console.log("step 1"));
await context.run("step-2", () => console.log("step 2"));
});
```
Trigger it from your backend:
```ts
import { Client } from "@upstash/workflow";
const client = new Client({ token: process.env.QSTASH_TOKEN! });
await client.trigger({ url: "https://your-app.com/api/workflow" });
```
## Other Skill Files
These files contain the full documentation. Use them for details, patterns, and advanced behavior.
- basics:
- **basics/serve** – How to expose workflow endpoints.
- **basics/context** – Full API for workflow `context` (steps, waits, webhooks, events, invoke, etc.).
- **basics/client** – Using the Workflow client to trigger, cancel, inspect, and notify runs.
- features:
- **features/invoke** – Cross‑workflow invocation.
- **features/reliability** – Retries, failure callbacks, and DLQ.
- **features/flow-control** – Rate limits, concurrency, and parallelism.
- **features/wait-for-event** – Notify and wait-for-event patterns.
- **features/webhooks** – Webhook creation and consumption.
- how to:
- **how-to/local-dev** – Local QStash dev server (auto via `QSTASH_DEV=true`) and tunneling.
- **how-to/realtime** – Realtime and human‑in‑the‑loop workflows.
- **how-to/migrations** – Migrating workflows safely.
- **how-to/middleware** – Adding middleware to workflows.
- other files:
- **rest-api** – Low-level REST endpoints for interacting with QStash/Workflow.
- **troubleshooting** – Common debugging and environment issues.
- **agents** – Using Workflow with agents, orchestrators, and automation patterns.
Referenced files: 15
Package details
Publisher declarations from the archived package. These are separate from our research and the live service's terms.
- Package author
- Upstash
Package observed Oct 2, 2026.
Technical details
- First seen
- Sep 30, 2026 · 22:02 UTC
- Last seen
- Oct 2, 2026 · 00:00 UTC
- Collection status
- Collected
plugin_asdk_app_6a79c45722208191816dac268a720cb5
Download plugin data (JSON)