Browse the web
Give your agents full access to the Chrome DevTools Protocol (CDP) with browser tools. Beta
Instead of a fixed set of browser actions (click, screenshot, navigate), the LLM writes JavaScript code that runs CDP commands against a live browser session — accessing all domains, commands, events, and types in the protocol.
Two tools are provided:
| Tool | Description |
|---|---|
browser_search | Query the CDP spec to discover commands, events, and types. The spec is fetched dynamically from the browser's CDP endpoint and cached. |
browser_execute | Run CDP commands against a live browser via a cdp helper. Each call opens a fresh browser session, executes the code, and closes it. |
Browser tools are useful when your agent needs to:
- Inspect web pages — DOM structure, computed styles, accessibility tree
- Debug frontend issues — network waterfalls, console errors, performance traces
- Scrape structured data — extract content from rendered pages
- Capture screenshots or PDFs — visual snapshots of web content
- Profile performance — Core Web Vitals, JavaScript profiling, memory analysis
For basic page fetches that do not need a rendered DOM, use fetch() instead.
Browser tools require the Agents SDK and @cloudflare/codemode:
npm install agents @cloudflare/codemode ai zodAdd the Browser Rendering and Worker Loader bindings to your wrangler configuration:
{ "compatibility_flags": ["nodejs_compat"], "browser": { "binding": "BROWSER" }, "worker_loaders": [ { "binding": "LOADER" } ]}compatibility_flags = [ "nodejs_compat" ]
[browser]binding = "BROWSER"
[[worker_loaders]]binding = "LOADER"import { createBrowserTools } from "agents/browser/ai";
const browserTools = createBrowserTools({ browser: env.BROWSER, loader: env.LOADER,});import { createBrowserTools } from "agents/browser/ai";
const browserTools = createBrowserTools({ browser: env.BROWSER, loader: env.LOADER,});To connect to a custom CDP endpoint instead of the Browser Rendering binding, pass cdpUrl.
Pass browser tools alongside your other tools. The model can be any AI SDK provider — here using Workers AI:
import { streamText } from "ai";import { createWorkersAI } from "workers-ai-provider";
const workersai = createWorkersAI({ binding: env.AI });
const result = streamText({ model: workersai("@cf/zai-org/glm-4.7-flash"), system: "You are a helpful assistant that can inspect web pages.", messages, tools: { ...browserTools, ...otherTools, },});import { streamText } from "ai";import { createWorkersAI } from "workers-ai-provider";
const workersai = createWorkersAI({ binding: env.AI });
const result = streamText({ model: workersai("@cf/zai-org/glm-4.7-flash"), system: "You are a helpful assistant that can inspect web pages.", messages, tools: { ...browserTools, ...otherTools, },});Both tools accept a code parameter containing a JavaScript async arrow function. The sandbox injects globals depending on the tool — spec for browser_search and cdp for browser_execute.
When the LLM uses browser_search, the code queries the CDP spec via the injected spec object:
async () => { const s = await spec.get(); return s.domains .find((d) => d.name === "Network") .commands.map((c) => ({ method: c.method, description: c.description }));};When the LLM uses browser_execute, the code runs CDP commands via the injected cdp helper:
async () => { const { targetId } = await cdp.send("Target.createTarget", { url: "https://example.com", }); const sessionId = await cdp.attachToTarget(targetId); const { root } = await cdp.send("DOM.getDocument", {}, { sessionId }); const { outerHTML } = await cdp.send( "DOM.getOuterHTML", { nodeId: root.nodeId }, { sessionId }, ); await cdp.send("Target.closeTarget", { targetId }); return outerHTML;};The typical pattern is to create browser tools inside an AIChatAgent message handler, which gives you message persistence and streaming:
import { AIChatAgent } from "@cloudflare/ai-chat";import { createBrowserTools } from "agents/browser/ai";import { createWorkersAI } from "workers-ai-provider";import { streamText, convertToModelMessages, stepCountIs } from "ai";
export class MyAgent extends AIChatAgent { async onChatMessage() { const workersai = createWorkersAI({ binding: this.env.AI }); const browserTools = createBrowserTools({ browser: this.env.BROWSER, loader: this.env.LOADER, });
const result = streamText({ model: workersai("@cf/zai-org/glm-4.7-flash"), system: "You can browse the web and inspect pages.", messages: await convertToModelMessages(this.messages), tools: { ...browserTools, }, stopWhen: stepCountIs(10), });
return result.toUIMessageStreamResponse(); }}import { AIChatAgent } from "@cloudflare/ai-chat";import { createBrowserTools } from "agents/browser/ai";import { createWorkersAI } from "workers-ai-provider";import { streamText, convertToModelMessages, stepCountIs } from "ai";
export class MyAgent extends AIChatAgent<Env> { async onChatMessage() { const workersai = createWorkersAI({ binding: this.env.AI }); const browserTools = createBrowserTools({ browser: this.env.BROWSER, loader: this.env.LOADER, });
const result = streamText({ model: workersai("@cf/zai-org/glm-4.7-flash"), system: "You can browse the web and inspect pages.", messages: await convertToModelMessages(this.messages), tools: { ...browserTools, }, stopWhen: stepCountIs(10), });
return result.toUIMessageStreamResponse(); }}For TanStack AI, use the /tanstack-ai export:
import { createBrowserTools } from "agents/browser/tanstack-ai";import { chat, workersAIText } from "@tanstack/ai";
const browserTools = createBrowserTools({ browser: env.BROWSER, loader: env.LOADER,});
const stream = chat({ adapter: workersAIText(env.AI, "@cf/zai-org/glm-4.7-flash"), tools: [...browserTools, ...otherTools], messages,});import { createBrowserTools } from "agents/browser/tanstack-ai";import { chat, workersAIText } from "@tanstack/ai";
const browserTools = createBrowserTools({ browser: env.BROWSER, loader: env.LOADER,});
const stream = chat({ adapter: workersAIText(env.AI, "@cf/zai-org/glm-4.7-flash"), tools: [...browserTools, ...otherTools], messages,});browser_searchfetches the live CDP protocol from the browser's/json/protocolendpoint and caches it briefly.browser_executeopens a fresh browser session for each call, exposes a smallcdphelper API to sandboxed code, and closes the session when execution finishes.- LLM-generated code runs in a Worker sandbox. CDP traffic stays in the host Worker.
Inside browser_execute, the following functions are available to the sandboxed code.
Send a CDP command and wait for the response.
| Parameter | Type | Description |
|---|---|---|
method | string | CDP method, for example "DOM.getDocument" or "Network.enable" |
params | unknown | Method parameters |
options.timeoutMs | number | Per-command timeout (default: 10 seconds) |
options.sessionId | string | Target session ID (required for page-scoped commands) |
Attach to a target and get a session ID. Uses Target.attachToTarget with flatten: true.
| Parameter | Type | Description |
|---|---|---|
targetId | string | The target to attach to |
options.timeoutMs | number | Timeout for the attach command |
Returns the sessionId string.
Get recent CDP debug log entries (sends, receives, errors). Defaults to the last 50 entries, max 400.
Clear the debug log buffer.
Returns AI SDK tools (browser_search and browser_execute).
| Option | Type | Default | Description |
|---|---|---|---|
browser | Fetcher | — | Browser Rendering binding |
cdpUrl | string | — | Optional override for a custom CDP endpoint |
cdpHeaders | Record<string, string> | — | Headers for CDP URL discovery (for example, Cloudflare Access) |
loader | WorkerLoader | required | Worker Loader binding for sandboxed execution |
timeout | number | 30000 | Execution timeout in milliseconds |
Either browser or cdpUrl must be provided. When both are set, cdpUrl takes priority.
For custom integrations, import the building blocks directly:
import { CdpSession, connectBrowser, connectUrl, createBrowserToolHandlers,} from "agents/browser";
// Connect to a custom CDP endpointconst session = await connectUrl("http://localhost:9222");const version = await session.send("Browser.getVersion");session.close();import { CdpSession, connectBrowser, connectUrl, createBrowserToolHandlers,} from "agents/browser";
// Connect to a custom CDP endpointconst session = await connectUrl("http://localhost:9222");const version = await session.send("Browser.getVersion");session.close();Recent Wrangler releases support Browser Rendering in local development. npx wrangler dev provisions the browser automatically, so the same browser: env.BROWSER setup works locally and when deployed.
Use cdpUrl only when you intentionally want to connect to some other CDP-compatible browser endpoint, such as a tunnel or a manually managed Chrome instance.
- LLM-generated code runs in isolated Worker sandboxes — each execution gets its own Worker instance
- External network access (
fetch,connect) is blocked in the sandbox at the runtime level - CDP commands are dispatched via Workers RPC — the WebSocket lives in the host, not the sandbox
- The CDP spec stays on the server — only query results flow to the LLM
- Responses are truncated to approximately 6,000 tokens to prevent context window overflow
- One session per execute call — each
browser_executeinvocation opens a fresh browser session. Multi-step workflows must be completed within a single code block. - No authenticated sessions — the browser starts without any cookies or login state.
- Requires
@cloudflare/codemodeas a peer dependency. - Limited to JavaScript execution in the sandbox (no TypeScript syntax).
If you prefer to control the browser programmatically without LLM-generated code, you can use Puppeteer with the Browser Rendering API directly.
npm i -D @cloudflare/puppeteer yarn add -D @cloudflare/puppeteer pnpm add -D @cloudflare/puppeteer bun add -d @cloudflare/puppeteer import puppeteer from "@cloudflare/puppeteer";
export class MyAgent extends Agent { async browse(browserInstance, urls) { let responses = []; for (const url of urls) { const browser = await puppeteer.launch(browserInstance); const page = await browser.newPage(); await page.goto(url);
await page.waitForSelector("body"); const bodyContent = await page.$eval( "body", (element) => element.innerHTML, );
let resp = await this.env.AI.run("@cf/zai-org/glm-4.7-flash", { messages: [ { role: "user", content: `Return a JSON object with the product names, prices and URLs from the website content below. <content>${bodyContent}</content>`, }, ], });
responses.push(resp); await browser.close(); }
return responses; }}import puppeteer from "@cloudflare/puppeteer";
interface Env { BROWSER: Fetcher; AI: Ai;}
export class MyAgent extends Agent<Env> { async browse(browserInstance: Fetcher, urls: string[]) { let responses = []; for (const url of urls) { const browser = await puppeteer.launch(browserInstance); const page = await browser.newPage(); await page.goto(url);
await page.waitForSelector("body"); const bodyContent = await page.$eval( "body", (element) => element.innerHTML, );
let resp = await this.env.AI.run("@cf/zai-org/glm-4.7-flash", { messages: [ { role: "user", content: `Return a JSON object with the product names, prices and URLs from the website content below. <content>${bodyContent}</content>`, }, ], });
responses.push(resp); await browser.close(); }
return responses; }}Add the browser binding to your wrangler configuration:
{ "ai": { "binding": "AI" }, "browser": { "binding": "BROWSER" }}[ai]binding = "AI"
[browser]binding = "BROWSER"You can also use Browserbase ↗ by using the Browserbase API directly from within your Agent.
Once you have your Browserbase API key ↗, you can add it to your Agent by creating a secret:
cd your-agent-project-foldernpx wrangler@latest secret put BROWSERBASE_API_KEYInstall the @cloudflare/puppeteer package and use it from within your Agent to call the Browserbase API:
npm i @cloudflare/puppeteer yarn add @cloudflare/puppeteer pnpm add @cloudflare/puppeteer bun add @cloudflare/puppeteer import puppeteer from "@cloudflare/puppeteer";
export class MyAgent extends Agent { async browse(url) { const browser = await puppeteer.connect({ browserWSEndpoint: `wss://connect.browserbase.com?apiKey=${this.env.BROWSERBASE_API_KEY}`, }); const page = await browser.newPage(); await page.goto(url); const content = await page.content(); await browser.close(); return content; }}import puppeteer from "@cloudflare/puppeteer";
interface Env { BROWSERBASE_API_KEY: string;}
export class MyAgent extends Agent<Env> { async browse(url: string) { const browser = await puppeteer.connect({ browserWSEndpoint: `wss://connect.browserbase.com?apiKey=${this.env.BROWSERBASE_API_KEY}`, }); const page = await browser.newPage(); await page.goto(url); const content = await page.content(); await browser.close(); return content; }}