diff --git a/box/guides/web-scraping-playwright.mdx b/box/guides/web-scraping-playwright.mdx
index 6357095b..90a52f34 100644
--- a/box/guides/web-scraping-playwright.mdx
+++ b/box/guides/web-scraping-playwright.mdx
@@ -4,6 +4,14 @@ title: "Scrape Dynamic Websites with Playwright"
In this guide, we use Upstash Box to run [Playwright](https://playwright.dev) against a JavaScript-heavy site, scrape structured data from it, and pull the results back to our own server. Because a Box is a real Linux container rather than a restricted serverless runtime, Chromium and its system dependencies install and run exactly like they would on your laptop.
+
+ Boxes also ship a managed, pre-installed browser. Create one with `browser:
+ true` and drive Chromium through the SDK, or [connect Playwright over
+ CDP](/box/overall/browser/connect) with no install step. This guide shows
+ the manual route, where the box's agent installs and controls Playwright
+ itself. See [Browser](/box/overall/browser/overview) for the built-in one.
+
+
---
## 1. Installation
diff --git a/box/overall/browser/ai-actions.mdx b/box/overall/browser/ai-actions.mdx
new file mode 100644
index 00000000..1263321b
--- /dev/null
+++ b/box/overall/browser/ai-actions.mdx
@@ -0,0 +1,113 @@
+---
+title: "AI Actions"
+---
+
+Beyond reading pages, a tab can act. A DOM-aware browser agent runs inside the box and resolves natural-language instructions against the live page. It can find elements, execute single actions, or complete multi-step tasks on its own.
+
+
+ AI actions use an LLM and are metered. They need an API key for the model's
+ provider (Anthropic, OpenAI, OpenRouter, Vercel, or OpenCode) on the box or
+ your account. Every method accepts a provider-prefixed `model` override such
+ as `"openai/gpt-4o"`. Without an override, the call uses the model the box
+ was configured with. If the box has no model, it falls back to
+ `anthropic/claude-sonnet-4-5`.
+
+
+## Observe
+
+`observe()` finds actionable elements matching an instruction. Use it to check the page before acting, or to build your own action loop:
+
+
+```typescript box.ts
+const { elements } = await tab.observe("find the login and signup buttons")
+
+for (const el of elements) {
+ console.log(el.description, el.selector)
+}
+```
+
+```python box.py
+result = tab.observe("find the login and signup buttons")
+
+for el in result.elements:
+ print(el.description, el.selector)
+```
+
+
+## Act
+
+`act()` resolves and executes exactly one action described in natural language:
+
+
+```typescript box.ts
+const action = await tab.act("click the primary call-to-action")
+
+console.log(action.success, action.actionDescription)
+console.log(action.inputTokens, action.outputTokens)
+```
+
+```python box.py
+action = tab.act("click the primary call-to-action")
+
+print(action.success, action.action_description)
+print(action.input_tokens, action.output_tokens)
+```
+
+
+The result reports what was done (`actions` with the resolved selectors), whether it succeeded, and the token usage of the call.
+
+## Run
+
+`run()` is the autonomous mode. The agent reads the page, acts, and repeats until the task is complete or it hits the step limit. Pass a schema to get structured data back at the end:
+
+
+```typescript box.ts
+import { z } from "zod"
+
+const { data, completed, steps } = await tab.run(
+ "Find the pricing page and summarize the free tier",
+ {
+ schema: z.object({ summary: z.string() }),
+ maxSteps: 15,
+ // model: "openai/gpt-4o", // any provider you hold a key for
+ },
+)
+
+console.log(completed, data.summary)
+for (const step of steps) {
+ console.log(step.step, step.action, step.url)
+}
+```
+
+```python box.py
+from pydantic import BaseModel
+
+class Summary(BaseModel):
+ summary: str
+
+result = tab.run(
+ "Find the pricing page and summarize the free tier",
+ schema=Summary,
+ max_steps=15,
+ # model="openai/gpt-4o", # any provider you hold a key for
+)
+
+print(result.completed, result.data.summary)
+for step in result.steps:
+ print(step.step, step.action, step.url)
+```
+
+
+- `maxSteps`: defaults to `15`, capped at `30`.
+- `schema`: optional. Without it, `run` returns its findings as text in `result`.
+- The result includes `completed`, a step-by-step trace in `steps` (each with the action taken, its reasoning, and the URL), and total token usage.
+
+## Which one to use
+
+| Method | Does | Best for |
+|---|---|---|
+| `observe` | Finds elements, executes nothing | Inspecting a page, building custom loops |
+| `act` | Executes one action | Flows where your code decides each step |
+| `run` | Executes a whole task autonomously | Open-ended or navigation-heavy tasks |
+
+For fully scripted control with no LLM in the loop, [connect over CDP](/box/overall/browser/connect) with Playwright or Puppeteer instead. Both drive the same tabs, so you can mix scripted steps with AI steps. To watch or replay what the agent did, see [Live View](/box/overall/browser/live-view) and [Recordings](/box/overall/browser/recordings).
diff --git a/box/overall/browser/connect.mdx b/box/overall/browser/connect.mdx
new file mode 100644
index 00000000..83bc5df3
--- /dev/null
+++ b/box/overall/browser/connect.mdx
@@ -0,0 +1,81 @@
+---
+title: "Connect over CDP"
+---
+
+The box browser is a real Chromium, and you can drive it with the tools you already use. `cdpUrl()` returns an authenticated Chrome DevTools Protocol WebSocket URL that Playwright, Puppeteer, or Stagehand can connect to directly. There is no browser to install and nothing to manage.
+
+
+```typescript box.ts
+const cdpUrl = await box.browser.cdpUrl()
+// wss://…?token=…
+```
+
+```python box.py
+cdp_url = box.browser.cdp_url()
+# wss://…?token=…
+```
+
+
+Like [live view](/box/overall/browser/live-view) URLs, the CDP URL carries its auth token in the URL. Anyone who has it gets full control of the browser, so treat it as a secret.
+
+## Playwright
+
+`playwright-core` is enough here, since you connect to the box's Chromium instead of launching one locally:
+
+
+```typescript scrape.ts
+import { chromium } from "playwright-core"
+
+const browser = await chromium.connectOverCDP(cdpUrl)
+
+const context = browser.contexts()[0] ?? (await browser.newContext())
+const page = context.pages()[0] ?? (await context.newPage())
+
+await page.goto("https://upstash.com")
+console.log(await page.title())
+```
+
+```python scrape.py
+from playwright.sync_api import sync_playwright
+
+with sync_playwright() as p:
+ browser = p.chromium.connect_over_cdp(cdp_url)
+ context = browser.contexts[0] if browser.contexts else browser.new_context()
+ page = context.pages[0] if context.pages else context.new_page()
+ page.goto("https://upstash.com")
+ print(page.title())
+```
+
+
+## Puppeteer
+
+```typescript scrape.ts
+import puppeteer from "puppeteer-core"
+
+const browser = await puppeteer.connect({ browserWSEndpoint: cdpUrl })
+
+const page = (await browser.pages())[0] ?? (await browser.newPage())
+await page.goto("https://upstash.com")
+```
+
+## Stagehand
+
+[Stagehand](https://www.stagehand.dev) can use the box browser as its local browser:
+
+```typescript agent.ts
+import { Stagehand } from "@browserbasehq/stagehand"
+
+const stagehand = new Stagehand({
+ env: "LOCAL",
+ localBrowserLaunchOptions: { cdpUrl },
+})
+
+await stagehand.init()
+await stagehand.act("click the first link")
+```
+
+## Mixing CDP and SDK control
+
+CDP clients and the SDK drive the same browser and the same tabs. A page opened by Playwright shows up in `box.browser.listTabs()`, and a tab created by the SDK is visible to Playwright. You can script the predictable steps like login and pagination with Playwright, hand the tab to [`act` or `run`](/box/overall/browser/ai-actions) for the steps that are easier to describe in natural language, and watch either through [Live View](/box/overall/browser/live-view).
+
+As a rule of thumb: use CDP when you want precise, repeatable scripting with no LLM in the loop. Use [AI Actions](/box/overall/browser/ai-actions) when describing the task is easier than scripting it.
diff --git a/box/overall/browser/live-view.mdx b/box/overall/browser/live-view.mdx
new file mode 100644
index 00000000..ee0de085
--- /dev/null
+++ b/box/overall/browser/live-view.mdx
@@ -0,0 +1,48 @@
+---
+title: "Live View"
+---
+
+Every tab can produce a live view URL: a shareable link that streams the tab in real time. Open it in any browser, or embed it in an `