# Browser automation

> Drive a real Chromium browser inside a sandbox over the Chrome DevTools Protocol. Connect Playwright, Puppeteer, browser-use, or Stagehand, or use the built-in screenshot and action helpers.

A **browser sandbox** is a regular Flax sandbox with a real, headful-capable Chromium running
inside it. You drive it over the [Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/)
(CDP), so any CDP-speaking tool works: [Playwright](https://playwright.dev),
[Puppeteer](https://pptr.dev), [browser-use](https://github.com/browser-use/browser-use),
[Stagehand](https://github.com/browserbase/stagehand), or your own client. This is the foundation
for web-browsing agents, scraping, screenshot services, and end-to-end test runners.

Everything else about the sandbox still applies: you get a `/workspace`, you can run commands and
manage files, and the browser profile and artifacts [persist](./concepts.md#persistence) across
stop/resume, [snapshots](./snapshots.md), and [forks](./forks.md).

## Create a browser sandbox

Browser sandboxes are requested with a **capability** rather than a template name. The platform
selects a Chromium-ready image for you.

```python
from flaxcloud import FlaxClient

flax = FlaxClient()
sb = flax.create_sandbox(capabilities=["browser"])
print(sb.capabilities)  # ["browser"]
```

```ts
import { FlaxClient } from "@flaxcloud/sdk";

const flax = new FlaxClient();
const sb = await flax.createSandbox({ capabilities: ["browser"] });
console.log(sb.capabilities); // ["browser"]
```

```bash
flax sandbox create --browser
```

## Connect over CDP (recommended)

The richest way to automate the browser is to connect your own framework to the sandbox's CDP
endpoint. `get_cdp_url()` (Python) / `getCdpUrl()` (JS) starts the browser if needed and returns a
short-lived, authenticated `wss://` URL.

```python
sb.browser.start()
cdp_url = sb.browser.get_cdp_url()   # short-lived wss:// URL

# Drive it with Playwright:
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.connect_over_cdp(cdp_url)
    page = browser.contexts[0].pages[0]
    page.goto("https://example.com")
    print(page.title())
```

```ts
await sb.browser.start();
const cdpUrl = await sb.browser.getCdpUrl(); // short-lived wss:// URL

// Drive it with Playwright:
import { chromium } from "playwright";

const browser = await chromium.connectOverCDP(cdpUrl);
const [context] = browser.contexts();
const [page] = context.pages();
await page.goto("https://example.com");
console.log(await page.title());
```

The CDP URL is **single-purpose and expires**. Mint a fresh one per automation run, and don't log
or share it: anyone holding it can drive your browser until it expires. There is a small cap on the
number of simultaneously active CDP URLs per sandbox; stop the browser or let URLs expire to free
slots.

## Built-in helpers (no framework)

For quick tasks you don't need an external framework. The SDK exposes a screenshot helper and a
high-level action endpoint.

```python
sb.browser.start()
png = sb.browser.screenshot_bytes()              # raw PNG bytes
with open("shot.png", "wb") as f:
    f.write(png)
```

```ts
await sb.browser.start();
const png = await sb.browser.screenshotBytes();  // Uint8Array of PNG bytes
```

```bash
flax browser start sbx_abc123
flax browser cdp-url sbx_abc123
flax browser screenshot sbx_abc123 -o shot.png
flax browser stop sbx_abc123
```

## Persistence

Browser state lives under `/workspace/.flax/browser/`:

- `profile` - cookies, local storage, logged-in sessions
- `screenshots` - captures you take
- `downloads` - files the browser downloads
- `traces` - Playwright/CDP traces you save there

Because these are normal `/workspace` files, they survive stop/resume and are copied by snapshots
and forks. This is **profile and artifact persistence**, not a live, resumable browser process: a
resumed sandbox starts a fresh Chromium that reuses the same profile and files. A common pattern is
to log into a site once, [snapshot](./snapshots.md) the sandbox, then fork it per task so every run
starts already authenticated.

## HTTP API

The SDKs wrap these endpoints. The browser lifecycle is per-sandbox:

| Method | Path | Purpose |
|--------|------|---------|
| `POST` | `/v1/sandboxes/{id}/browser/start` | Start Chromium in the sandbox. |
| `GET`  | `/v1/sandboxes/{id}/browser/status` | Report whether the browser is up. |
| `GET`  | `/v1/sandboxes/{id}/browser/connect` | Mint a short-lived authenticated CDP URL. |
| `POST` | `/v1/sandboxes/{id}/browser/screenshot` | Capture a PNG (base64 in the JSON body). |
| `POST` | `/v1/sandboxes/{id}/browser/action` | Run a high-level action (navigate, click, type, ...). |
| `POST` | `/v1/sandboxes/{id}/browser/stop` | Stop Chromium and revoke active CDP URLs. |

See the [API reference](./api.md) for request and response shapes.

## Notes and limits

- Browser sandboxes use more memory than a plain `python`/`node` sandbox; pick a memory size with
  headroom for Chromium (see [Plans & limits](./plans-and-limits.md)).
- A browser sandbox needs network access, so it can't use `network_mode: "none"`.
- Treat any site you visit as untrusted input to your agent. The sandbox isolates the browser from
  the host, but your automation logic still decides what to click and submit. See
  [Security](./security.md).
