Give Your Box a Browser
Every Upstash Box can now come with its own browser. Create a box with browser: true and you get a managed, headless Chromium you control through the SDK. Open tabs, read pages, take screenshots, extract structured data, run AI agents on the live DOM, or connect Playwright straight over CDP.
The interesting part is not the browser. It's where the browser lives.
- Add
browser: trueat create time and every box gets a headless Chromium. - Open your app, click through a flow, screenshot, and extract schema-validated data off the live DOM.
- Drive it two ways: raw Playwright/CDP, or single natural-language actions on the live page.
- It lives inside a real computer, so you can browse a page and then verify, fix, and commit without leaving the box.
A browser wants a computer around it
Cloud browsers are not new. You can rent a headless Chromium from a handful of services, point Playwright at it, and load a page. That part is solved.
The problem shows up right after. Your agent opens a page, sees what is on it, and then what? Something has to happen next. A screenshot gets saved, a failing flow gets traced back to the code, a fix gets written and committed to a repo. A browser on its own can't do any of that. So you wire the browser to a second environment that can, and now you are running two things that have to find each other, share credentials, and pass data back and forth.
A box already is that second environment. It has a full shell, a persistent filesystem, git, and a coding agent, all in one isolated container. Putting the browser inside it means the page you just loaded and the code that acts on it are on the same machine. Nothing to wire up.
This matters most when you are not the one watching. On your own laptop the wiring is already free: you have a browser, a terminal, and the repo open side by side. An agent has none of that. It needs to start the app, look at what the page actually did, and change the code with no human in the middle, on twenty branches at once, on machines you are not sitting at.
import { Box } from "@upstash/box"
const box = await Box.create({
runtime: "node",
browser: true,
})
// Start your app in the box's shell, then wait for it to listen
await box.exec.command("nohup npm run dev > dev.log 2>&1 &")
await box.exec.command("until curl -sf localhost:3000 > /dev/null; do sleep 1; done")
// Open it in the box's browser, boots Chromium on first use
const tab = await box.browser.tab.create("http://localhost:3000")
// Capture the page as a PNG, no display needed
const png = await tab.screenshot()Everything is headless. There is no desktop, no VNC, nothing to install. Chromium is provisioned with the box and boots on the first tab.
What you get
box.browser manages the browser itself. Page-level operations live on a Tab handle, addressed by its CDP target id so it stays valid across navigations and even across processes.
Read any page - content() returns the title, visible text, and links from the real DOM, including JavaScript-rendered content.
Screenshots - screenshot() captures the tab as a PNG, full-page or viewport, with no display needed.
Schema-validated extraction - extract() hands the page to a DOM-aware agent and returns data parsed against your Zod or pydantic schema, so a successful call always gives you the shape you asked for.
Tabs that survive - open as many as you want, list them, and re-attach to one from a different process by its id.
Live view - a tokenized, view-only stream of any tab you can embed in an iframe.
Recordings - capture a session to replayable video with chapter markers for each tab switch and agent run.
Bring your own tools - cdpUrl() returns an authenticated endpoint you can drive with Playwright, Puppeteer, or Stagehand.
Two ways to drive it
The same browser answers to two levels of control, and you can mix them on the same tab.
Raw and deterministic. Point Playwright at the box over CDP and run your existing scripts. No LLM in the loop.
import { chromium } from "playwright-core"
const cdpUrl = await box.browser.cdpUrl()
const browser = await chromium.connectOverCDP(cdpUrl)One action at a time. observe() finds the actionable elements on a page and act() resolves and executes a single instruction against the live DOM. Good for flows where your code decides each step. A resolved action replays with no LLM, so you can cache the expensive step and reuse it.
await tab.act("click the sign-in button")Because both drive the same tabs, you can script the brittle parts, like login and pagination, with Playwright, then hand the tab to act for the fuzzy parts.
Browse, verify, then act
Here is the part a standalone browser can't do. This box starts an app, opens it in Chromium, walks a flow, and checks what actually happened. Same container, start to finish.
import { Box } from "@upstash/box"
import { z } from "zod"
const box = await Box.create({ runtime: "node", browser: true })
// 1. Start the app in the box's shell, then wait for it to listen
await box.exec.command("nohup npm run dev > dev.log 2>&1 &")
await box.exec.command("until curl -sf localhost:3000 > /dev/null; do sleep 1; done")
// 2. Open it in the box's browser
const tab = await box.browser.tab.create("http://localhost:3000")
// 3. Drive the flow with a real click
await tab.act("submit the signup form with the email field left blank")
const png = await tab.screenshot()
// 4. Check the result against a schema, off the live DOM
const check = await tab.extract(
"did the form submit, and is a validation error visible?",
z.object({
submitted: z.boolean(),
errorMessage: z.string().nullable(),
}),
)
// → { submitted: true, errorMessage: null }submitted: true with nothing shown to the user is the bug, and the check found it rather than you. The box has a coding agent looking at the same filesystem the app runs from, so it can take that finding, fix the code, and commit, without anything leaving the container:
// 5. Fix: hand the finding to the agent
await box.agent.run({
prompt:
`The signup form submitted with an empty email and showed no error ` +
`(submitted: ${check.submitted}, errorMessage: ${check.errorMessage}). ` +
`Add validation and a visible error message.`,
})
// 6. Commit: git is right there too
await box.git.commit({ message: "Validate email on signup" })No second service, no data passed between environments, no glue. The browser, the app, the filesystem, and git are the same machine.
Browse with the browser, verify with a real click, fix with the agent, ship with git. One box.
Try it
Create a box with browser: true, open a tab, and read a page. That is the whole setup. From there the extraction, the AI actions, and the CDP connection are one call away.
The browser guide walks through each piece. The free tier is enough to try it, so grab an API key from the Upstash Console, and see how much of your browse-verify-and-act workflow fits in a single box.
We'd love to hear what you build. Reach out anytime on Discord.
