For AI agents: a documentation index is available at /llms.txt
Skip to main content

Stagehand integration

Stagehand is an open-source SDK for browser agents that mixes natural-language actions (act(), observe(), extract()) with Playwright-style page code. This guide connects Stagehand v4 to a Browserless cloud browser, so you can run Stagehand automations without hosting Chrome yourself.

Prerequisites
  • Node.js 22.18 or later (required by Stagehand v4)
  • Browserless API token (available in your account dashboard)
  • An Anthropic or OpenAI API key (Stagehand also supports Google, Groq, and Cerebras)
  • Familiarity with Browserless browser extensions

How Stagehand v4 connects to Browserless​

Stagehand v4 runs its engine as a Chrome extension inside the browser, and that extension opens its own CDP connection back to the browser. That changes two things compared with Stagehand v3:

  • The extension has to be preinstalled. localBrowser.connect() tries to load the extension from a folder on your machine, which a remote browser can't read. You upload the extension to Browserless once and load it with the extensions launch parameter.
  • The extension has to dial the same session. A regular Browserless WebSocket URL starts a new browser on every connection, so the extension would end up in a different browser from your script. You call Browserless.reconnect to get a URL for the current session, then pass it to Stagehand as both cdpUrl and extensionCdpUrl.

If you'd rather skip both steps, see Using Stagehand v3.

Step-by-step setup​

  1. Set your API keys

    Go to your Browserless account dashboard and copy your API token. Then set it in a .env file or your shell, with one LLM key. The script uses whichever key is set, and prefers Anthropic if both are.

    BROWSERLESS_API_KEY=your-browserless-token
    ANTHROPIC_API_KEY=your-anthropic-key
  2. Install dependencies

    Stagehand v4 pins Zod 4.4. You'll also need puppeteer-core to call Browserless.reconnect.

    npm install @browserbasehq/stagehand 'zod@~4.4.3' puppeteer-core
  3. Upload the Stagehand extension

    Stagehand ships its extension inside the npm package. Export it as a ZIP file:

    // export-extension.mjs
    import { getExtensionArchive } from "@browserbasehq/stagehand";
    import { writeFile } from "node:fs/promises";

    await writeFile("stagehand-extension.zip", await getExtensionArchive());
    node export-extension.mjs

    Then upload stagehand-extension.zip on the Extensions page of your account dashboard and name it stagehand. Wait until its status is active. To upload from a script or CI pipeline instead, see Manage extensions from CI/CD.

    You only do this once per Stagehand version. Re-export and re-upload the extension whenever you upgrade @browserbasehq/stagehand, so the extension matches the SDK.

  4. Connect and run your automation

    The script below starts a session with the stagehand extension loaded, gets a reconnect URL for it, and connects Stagehand to that URL. It then opens the Browserless site, clicks through to the docs with act(), and pulls the doc sections out with extract():

    import { localBrowser, Stagehand } from "@browserbasehq/stagehand";
    import puppeteer from "puppeteer-core";
    import { z } from "zod/v4";

    const TOKEN = process.env.BROWSERLESS_API_KEY;
    const BROWSERLESS_URL = "wss://production-sfo.browserless.io";

    // Start a session with the Stagehand extension loaded, then get a URL that points back
    // to that same browser. The extension dials this URL from inside Chrome, so it must not
    // start a new session.
    async function getReconnectUrl() {
    const launch = JSON.stringify({ extensions: ["stagehand"] });
    const browser = await puppeteer.connect({
    browserWSEndpoint: `${BROWSERLESS_URL}?token=${TOKEN}&launch=${encodeURIComponent(launch)}`,
    });
    const [page] = await browser.pages();
    const cdp = await page.createCDPSession();
    const { browserWSEndpoint, error } = await cdp.send("Browserless.reconnect", {
    timeout: 60000,
    });
    if (error) throw new Error(error);
    await browser.disconnect();
    return `${browserWSEndpoint}?token=${TOKEN}`;
    }

    // Use whichever LLM key is set. Stagehand also accepts google/, groq/, and cerebras/ models.
    function getModel() {
    if (process.env.ANTHROPIC_API_KEY) {
    return { modelName: "anthropic/claude-sonnet-5-5", apiKey: process.env.ANTHROPIC_API_KEY };
    }
    if (process.env.OPENAI_API_KEY) {
    return { modelName: "openai/gpt-5.4-mini", apiKey: process.env.OPENAI_API_KEY };
    }
    throw new Error("Set ANTHROPIC_API_KEY or OPENAI_API_KEY.");
    }

    // Pick the model before starting a session, so a missing key doesn't use one.
    const model = getModel();

    const reconnectUrl = await getReconnectUrl();
    let browser;
    try {
    browser = await localBrowser.connect({
    cdpUrl: reconnectUrl,
    extensionCdpUrl: reconnectUrl,
    });
    } catch (error) {
    // The session is waiting for a reconnect, so it stays open until the reconnect timeout.
    // Reconnect once to close it. If that fails too, report the original error instead.
    try {
    const session = await puppeteer.connect({ browserWSEndpoint: reconnectUrl });
    await session.close();
    } catch {}
    throw error;
    }

    // The Stagehand extension keeps its own connection to the session, so a run that throws
    // before browser.close() leaves the session open until it times out.
    try {
    const stagehand = await Stagehand.create({ browser, model });

    const [page] = await browser.context.pages();
    await page.goto("https://www.browserless.io");

    await stagehand.act("click on the Docs link in the navigation");

    const { data } = await stagehand.extract(
    "extract the main documentation categories or sections",
    z.object({
    sections: z.array(
    z.object({
    title: z.string(),
    description: z.string(),
    }),
    ),
    }),
    );
    console.log(data.sections);

    await stagehand.close();
    } finally {
    await browser.close();
    }

    The timeout passed to Browserless.reconnect only needs to cover the gap between disconnecting Puppeteer and connecting Stagehand. In v4, act(), observe(), and extract() return a result object, so read the extracted value from data.

    Response (trimmed; the exact sections depend on the live site):

    [
    {
    title: 'Documentation',
    description: 'Guides, references, and examples for Browserless'
    },
    {
    title: 'QUICK REFERENCE',
    description: 'Connection URLs and Endpoints; Launch Parameters'
    },
    {
    title: 'INTEGRATIONS',
    description: 'Browser Use; Stagehand; LangChain; Vercel AI SDK; ...'
    },
    ...
    ]

Using Stagehand v3​

Stagehand v3 drives the browser over CDP from your machine, so it connects to a regular Browserless URL with no extension or reconnect step. Install the v3 line:

npm install @browserbasehq/stagehand@3 zod

Then pass the Browserless WebSocket URL as cdpUrl:

import { Stagehand } from "@browserbasehq/stagehand";

const stagehand = new Stagehand({
env: "LOCAL",
localBrowserLaunchOptions: {
cdpUrl: `wss://production-sfo.browserless.io?token=${process.env.BROWSERLESS_API_KEY}`,
},
model: "anthropic/claude-sonnet-4-5",
});

await stagehand.init();

const page = stagehand.context.pages()[0];
await page.goto("https://www.browserless.io");
await stagehand.act("click on the Docs link in the navigation");

await stagehand.close();

v3 reads the provider key from the environment, so set ANTHROPIC_API_KEY for this example. v3's act() doesn't work with current OpenAI models, because OpenAI rejects its response schema; use v4 if you want OpenAI. v3 is still published under the v3-latest npm tag, but new Stagehand features ship in v4.

Why use Browserless with Stagehand​

  • No infrastructure: skip managing Chrome installations, updates, or scaling
  • Global regions: choose US West (San Francisco), Europe (London), or Europe (Amsterdam) endpoints for lower latency
  • Parallel sessions: run multiple Stagehand sessions at the same time
  • Stealth and proxies: use stealth mode and residential proxies on sites that block automated browsers

Resources​

FAQ & Troubleshooting​

localBrowser.connect() fails with File path cannot be resolved

Stagehand couldn't find its extension in the browser, so it tried to load it from a folder on your machine, which the remote browser can't read. Upload the extension as shown in step 3 and include launch={"extensions":["stagehand"]} when you start the session.

act() or observe() fails with active target not registered

The extension is connected to a different browser from your script. This happens when you pass a regular Browserless URL as cdpUrl, because the extension's connection starts a new session. Use the URL returned by Browserless.reconnect and pass it as both cdpUrl and extensionCdpUrl, as shown in step 4.

Stagehand fails after I upgraded @browserbasehq/stagehand

The extension in your Browserless account no longer matches the SDK version. Re-run export-extension.mjs, then upload the new ZIP under the same stagehand name. Uploading the same name replaces the old extension; wait for its status to become active before starting new sessions.

extract() fails with Invalid schema for response_format

OpenAI's structured outputs require every field in the schema. Replace .optional() with a required field, or use .nullable() if the value can be missing.

Which LLM models can I use with Stagehand?

The sample's getModel() uses Claude when ANTHROPIC_API_KEY is set and GPT when OPENAI_API_KEY is set. To use another provider, return a model object with a provider/model name and that provider's API key, such as { modelName: "google/gemini-3.5-flash", apiKey: process.env.GOOGLE_API_KEY }. Stagehand v4 supports openai/, anthropic/, google/, groq/, and cerebras/ model names.

Next steps​

Was this page helpful?