Generated banner for Browser Use To Stagehand
Generated logo for Browser Use To Stagehand

Browser Use To Stagehand

BrowserbaseOfficialPublished by Browserbase

  • Skill
  • Workflow
  • Software development
  • MIT
  • No issues found

From the author

Migrate browser-use (Python) browser-automation scripts to Stagehand v3 (TypeScript) on Browserbase. Use when the user wants to convert, port, rewrite, or migrate a browser-use Agent script to Stagehand, map browser-use features/APIs to Stagehand primitives (act/extract/observe/agent), or move agentic browser automation onto Browserbase with more determinism. Triggers on "browser-use", "browser_use", or "Agent(task=...)".
— the description by Browserbase

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

This skill provides a structured methodology for porting browser-use (Python) agentic scripts to Stagehand v3 (TypeScript) on Browserbase. Rather than a direct transpilation, it emphasizes refactoring opaque agent loops into inspectable, deterministic pipelines. By applying a determinism spectrum, it helps developers replace LLM-heavy agentic steps with specific Stagehand primitives (act, extract, observe) where the page flow is predictable, reserving AI-driven agents only for genuinely open-ended tasks.

What it does

  • Analyzes browser-use scripts to extract tasks, browser configurations, and custom actions.
  • Maps agentic browser-use steps to Stagehand v3 primitives based on a determinism decision tree.
  • Generates runnable TypeScript code using Stagehand v3 and Zod for structured data extraction.
  • Provides setup templates for package.json, environment variables, and index.ts.
  • Offers a trace-assisted workflow for debugging and migrating flaky or opaque agent scripts.
  • Ensures best practices for secret management and browser session handling.

When to use it

  • When you need to migrate an existing browser-use Python script to a more stable TypeScript environment.
  • When you want to improve the reliability and speed of browser automation by reducing unnecessary LLM calls.
  • When you need to move agentic workflows onto the Browserbase platform for better observability.
  • When you are refactoring an opaque, flaky agent script into a maintainable, deterministic pipeline.

Advantages

  • Increases automation reliability by replacing non-deterministic agent loops with predictable primitives.
  • Reduces token costs and latency by using AI only where the page structure is unpredictable.
  • Improves maintainability through structured, inspectable TypeScript code.
  • Leverages Browserbase platform features for better session management and debugging.

Usage examples

“Migrate my browser-use script that searches for products on Amazon and extracts prices.”
The agent will inventory your script, identify the search and extraction steps, and provide a Stagehand v3 script using page.goto for navigation and stagehand.extract for price gathering.
“I have a flaky browser-use agent that logs into a portal. How do I move it to Stagehand?”
The agent will guide you through mapping the login flow to a deterministic Stagehand sequence, potentially using Browserbase context to handle authentication reliably.

Install

  1. Download the skill as a ZIP:

    https://autogineer.com/agent-skills/browserbase/browser-use-to-stagehand/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip browser-use-to-stagehand.zip -d .agents/skills

Documentation by Browserbase

browser-use → Stagehand on Browserbase (/browser-use-to-stagehand)

Convert a browser-use (Python) script into an idiomatic Stagehand v3 (TypeScript) script on Browserbase, choosing the right level of determinism at each step rather than producing a one-to-one agentic copy.

Core principle: browser-use is agentic-by-default (the LLM decides every action). Stagehand lets you choose how much AI to use. A good migration replaces opaque agent loops with an inspectable, mostly-deterministic pipeline — using AI only where the page is genuinely unpredictable. This is a refactor with judgment, not a transpile.

Source of truth & versions. This skill's durable value is the judgment — the determinism spectrum and the decompose-vs-agent decision — not the API specifics, which drift every release. The code mappings here are a snapshot validated against @browserbasehq/stagehand 3.6.x and browser-use 0.13.x (2026-06). On any conflict, the live docs win — always verify against the installed package and these sources before emitting code:

If the installed Stagehand major is not 3, treat this skill as conceptual only and follow the live docs for every signature.

Reference files (read as needed)

  • references/api-mapping.md — the mechanical browser-use → Stagehand mapping: variant detection, the full feature table, before/after code, Browserbase platform options, and v3 version gotchas. Read this for any non-trivial construct.

  • references/determinism.md — how to choose agent() vs act/extract/observe vs cached observe→act. The decision tree. Read this when deciding how to translate an Agent(task=…).

  • references/trace-assisted.md — the optional "run it on Browserbase, read the logs, then rewrite" workflow for opaque/flaky scripts.

  • references/guide.md — the human migration guide: philosophy shift, feature mapping, the determinism spectrum, and a recommended migration path.

  • references/prompt.md — a self-contained, tool-agnostic version of this skill; paste it into any AI assistant along with a browser-use script.

  • EXAMPLES.md — before/after script pairs.

Workflow

1. Get the source

Obtain the browser-use script(s). If the user only described a script, ask for the file(s). Note the target: TypeScript Stagehand on Browserbase unless they say otherwise.

First, gate on scope — is this even migratable? Not every browser-use file is an Agent(task=…) script. If the source is browser-use running as an MCP server (uvx browser-use --mcp, a mcpServers config) there is no Stagehand equivalent — flag it as out of scope, don't invent one (see api-mapping §3.7b). If the browser-use call is embedded in a larger app (a class/tool wrapper, web route, queue task), convert only the browser-use surface and preserve the surrounding app glue — see api-mapping §3.8.

2. Detect the browser-use variant

Identify legacy (pre-0.12) vs stable vs Rust beta (only when imports come from browser_use.beta) — see api-mapping §1. Note: the classic top-level from browser_use import Agent, ChatBrowserUse surface is alive and well in 0.13.x — ChatBrowserUse alone is not a beta tell; only a browser_use.beta import is. All variants translate identically, so when unsure, proceed with the stable mapping. Normalize legacy names before translating. State which variant you found.

3. Inventory the script

Extract a structured inventory before writing any TypeScript:

  • Task(s) — the task= string(s); split each into its implied ordered steps.

  • Model — the Chat* provider + model id.

  • Browser config — local vs cdp_url/Browserbase; headless; proxies; user_data_dir/storage_state.

  • Structured output — any output_model_schema Pydantic models.

  • Secrets — sensitive_data, env-var usage, login flows.

  • Guardrails — allowed_domains, max_steps.

  • Custom actions — @tools.action / Controller functions, and whether each is a deterministic side-effect or an agent capability.

  • Setup — initial_actions, secondary models (page_extraction_llm, planner_llm).

4. Decide the determinism level per step

For each step from the inventory, apply the decision tree in determinism.md:

  • Navigate to a known URL → page.goto(url) on the Stagehand page (no AI).

  • On-page action → act("…"); if it repeats, observe() once then replay act(action) (no LLM call).

  • Reading data → extract("…", zodSchema).

  • Genuinely open-ended → keep stagehand.agent().execute(...) (tightened with maxSteps/systemPrompt).

Default to decomposition when the flow is known; keep agent() only where it isn't. For a first lift-and-shift, a faithful agent() translation is acceptable — say so and note the optimization path.

5. Produce the Stagehand v3 rewrite

First, verify the API. Before writing, confirm the exact signatures you're about to use against the installed package (node_modules/@browserbasehq/stagehand types) or https://docs.stagehand.dev/v3. The mappings below are a 3.6.x snapshot; if anything differs in the installed version, the installed version wins. Then emit runnable TypeScript. Always:

  • import { Stagehand } from "@browserbasehq/stagehand"; and import { z } from "zod"; when extracting.

  • Get the page via const page = stagehand.context.pages()[0];.

  • Call AI methods on the instance: stagehand.act(...), stagehand.extract(...), stagehand.observe(...) — never page.act(...).

  • Set the model as a "provider/model" string.

  • Default to env: "BROWSERBASE"; show env: "LOCAL" as the dev option.

  • Pass secrets via variables and process.env, never hardcoded.

  • await stagehand.init() at the start, await stagehand.close() in a finally.

Include the project setup so it runs (see the templates below).

6. Write the migration summary

Alongside the code, produce a short summary:

  • Variant detected and the determinism choices made (which steps became deterministic vs AI vs agent), with the reasoning.

  • Needs human review — anything that didn't map 1:1: lost allowed_domains guardrails, custom-action logic, secondary-model intent, ambiguous task strings.

  • Recommended next step — Browserbase Context for auth reuse, caching for production, or the trace-assisted path if the flow was opaque.

7. Offer the trace-assisted path (only if warranted)

If the source was one large opaque agent(task=…), was flaky, or your rewrite can't be confidently mapped, offer the trace-assisted workflow (trace-assisted.md): run the original on Browserbase, pull sessions.logs.list, and rewrite from observed behavior. Don't run anything without the user's go-ahead.

Output templates

package.json

{
  "name": "stagehand-migration",
  "type": "module",
  "scripts": { "start": "tsx index.ts" },
  "dependencies": {
    "@browserbasehq/stagehand": "^3.0.0",
    "dotenv": "^16.0.0",
    "zod": "^3.25.0"
  },
  "devDependencies": { "tsx": "^4.0.0", "typescript": "^5.0.0" }
}

Add "ai": "^5.0.0" (Vercel AI SDK) only if a custom browser-use action maps to an agent tool. Pin v5, not v4 — Stagehand 3.6.x bundles ai v5 and types agent({ tools }) as the v5 ToolSet, where a tool's schema field is inputSchema. The v4 tool() helper emits parameters instead and will fail to type-check against Stagehand's v5 ToolSet. If you can't control the hoisted ai version, skip the tool() helper and pass a plain object { description, inputSchema: zodSchema, execute } — it satisfies the v5 ToolSet regardless of which ai major resolves.

.env

BROWSERBASE_API_KEY=...
BROWSERBASE_PROJECT_ID=...
ANTHROPIC_API_KEY=...   # or the provider matching your model string

index.ts skeleton (decomposed, the preferred shape)

import "dotenv/config";
import { Stagehand } from "@browserbasehq/stagehand";
import { z } from "zod";

async function main() {
  const stagehand = new Stagehand({
    env: "BROWSERBASE",
    model: "anthropic/claude-sonnet-4-6",
  });
  await stagehand.init();
  try {
    const page = stagehand.context.pages()[0];

    await page.goto("https://example.com");          // deterministic skeleton
    await stagehand.act("…");                          // AI where the page varies
    const data = await stagehand.extract("…", z.object({ /* … */ }));  // structured reads

    console.log(data);
  } finally {
    await stagehand.close();
  }
}

main().catch((err) => { console.error(err); process.exit(1); });

Validation checklist (before declaring done)

  • [ ] AI methods are on the instance (stagehand.act/extract/observe), not the page.

  • [ ] Page obtained via stagehand.context.pages()[0].

  • [ ] Model is a "provider/model" string; the matching provider key is in .env.

  • [ ] extract uses a zod schema; zod is in dependencies.

  • [ ] Secrets use variables + process.env; nothing hardcoded.

  • [ ] init() / close() present; close() in finally.

  • [ ] Each browser-use step is accounted for, placed deliberately on the determinism spectrum.

  • [ ] Migration summary lists determinism choices and "needs human review" items.

Common mistakes to avoid

  • Copying v2 syntax (page.act(), stagehand.page, modelName/modelClientOptions, enableCaching) from old blog posts. Use v3 — see api-mapping "Version notes".

  • Translating every step into act() — navigate with page.goto and cache repeatable steps via observe→act; don't spend an LLM call on every action.

  • Defaulting everything to agent() — that just reproduces browser-use's non-determinism in a new framework. Decompose where the flow is known.

  • Silently dropping allowed_domains — Stagehand has no domain firewall; flag it for review.

  • Inventing Browserbase/Stagehand options — if unsure of a field, check https://docs.stagehand.dev/v3 / https://docs.browserbase.com rather than guessing.

Files

references/api-mapping.md

browser-use → Stagehand + Browserbase: API Mapping

The authoritative, mechanical mapping the /browser-use-to-stagehand skill uses to translate code. Pair it with determinism.md (which Stagehand primitive to reach for) and trace-assisted.md (the optional run-and-observe path).

Stagehand examples here target v3 (@browserbasehq/stagehand ≥ 3.x), the current major version. v3 is a rewrite — see the Version notes at the bottom before trusting any older snippet you find online.

⚠️ This is a point-in-time snapshot (validated against Stagehand 3.6.x / browser-use 0.13.x, 2026-06), not a live spec. Signatures and option names drift every release. The live docs supersede this table on any conflict — verify against the installed package (node_modules/@browserbasehq/stagehand) or https://docs.stagehand.dev/v3 / https://docs.browserbase.com / https://docs.browser-use.com before relying on an exact signature.


1. Detect the browser-use variant first

browser-use is mid-transition across three API shapes. Identify which one the source script uses before translating, because the imports and class names differ.

VariantTell-tale imports / callsNotes
Legacy (pre-0.12)from browser_use import Browser, BrowserConfig; Browser(config=BrowserConfig(...)); BrowserContext; Controller(), @controller.actionDeprecated. Normalize to current names first (see §6), then translate.
Stable (0.12.x)from browser_use import Agent, Browser, BrowserProfile, Tools, ChatOpenAI ; Browser(browser_profile=BrowserProfile(...)) ; Tools(), @tools.actionWhat essentially all teams run today. The primary migration source.
Rust beta (0.13.x)imports specifically from from browser_use.beta import ...Opt-in new loop. The only reliable tell is the browser_use.beta import path. Translate the same way — the public surface (Agent(task=, llm=), agent.run()) is identical.

Note on 0.13.x: the classic top-level from browser_use import Agent, ChatBrowserUse, ChatOpenAI surface is still the default in 0.13.x — ChatBrowserUse is browser-use's hosted-model class and appears in stable code, not just the beta. Don't classify a script as "Rust beta" just because it imports ChatBrowserUse; require a literal browser_use.beta import. When in doubt, use the stable mapping (all variants translate the same way anyway).

Key renames to recognize (all three may appear in a codebase):

  • Browser is now an alias for BrowserSession — same class. Old BrowserContext is gone (folded into the session).

  • Controller is a backwards-compat alias for Tools; @controller.action ≡ @tools.action.

  • In custom actions, the injected browser param must be named exactly browser_session: BrowserSession.


2. Top-level mapping table

browser-use (Python)Stagehand v3 (TypeScript) / Browserbase
Agent(task=..., llm=...) + await agent.run()Decompose into stagehand.act / extract / observe when the flow is known (preferred); else faithful stagehand.agent().execute(...). See determinism.md.
llm=ChatAnthropic(model="claude-sonnet-4-6")new Stagehand({ model: "anthropic/claude-sonnet-4-6" }) (or per-call { model })
llm=ChatOpenAI(model="gpt-5")model: "openai/gpt-5"
llm=ChatGoogle(model="gemini-2.5-flash")model: "google/gemini-2.5-flash"
llm=ChatBrowserUse() (default)pick a provider string, e.g. "google/gemini-2.5-flash" (fast/cheap) or "anthropic/claude-sonnet-4-6"
agent.run(max_steps=30)agent().execute({ instruction, maxSteps: 30 })
output_model_schema=MyPydanticModelPreferred: decompose and stagehand.extract("...", zodSchema) for the typed read. For an agentic run, agent().execute({ output: zodObjectSchema }) — but see the two gotchas below.
history.final_result()extract(...) return value, or result.message from an agent run
history.structured_outputtyped extract(...) return, or result.output from agent().execute({ output }) (gotchas below)

Agent output gotchas (both bite at runtime, not compile time — verified on 3.6.x):

  1. Requires experimental mode. agent().execute({ output }) throws ExperimentalNotConfiguredError unless the Stagehand constructor sets experimental: true. Note experimental mode changes execution (it bypasses the managed Stagehand API path), so prefer the alternative below for production. This applies to three agent features — output schema, custom tools (§3.5), and MCP integrations (§3.7) all require experimental: true (validated together at agent creation). If you keep any of them, set it on the constructor and note the managed-API-bypass tradeoff in the migration summary.

  2. Must be a zod object (output?: StagehandZodObject) — unlike extract, a top-level z.array(...) is rejected. Wrap lists: z.object({ items: z.array(...) }).

Recommended non-experimental pattern for a typed result from an agentic task: run the agent without output, then do a separate stagehand.extract("...", zodSchema) on the final page (or over result.message). This keeps the managed API path and lets extract use a top-level array. | Browser() (local default) | new Stagehand({ env: "LOCAL", localBrowserLaunchOptions: { ... } }) | | Browser(cdp_url=session.connect_url) (Browserbase) | new Stagehand({ env: "BROWSERBASE" }) — Stagehand creates & manages the session | | BrowserProfile(headless=False) | localBrowserLaunchOptions: { headless: false } | | BrowserProfile(proxy=...) / Browserbase proxies | browserbaseSessionCreateParams: { proxies: true } | | BrowserProfile(user_data_dir=...) / storage_state=... | Browserbase Context (browserSettings.context: { id, persist: true }) or LOCAL localBrowserLaunchOptions.userDataDir | | BrowserProfile(allowed_domains=[...]) | No first-class equivalent — see §5 Gaps | | sensitive_data={...} | act("...%key%...", { variables: { key } }) | | initial_actions=[...] | plain deterministic code before the AI calls: await page.goto(...), etc. | | Tools() / @tools.action(...) | agent({ tools: { name: tool({...}) } }) (Vercel AI SDK), or just plain TS for deterministic side-effects | | use_vision=True / False / "auto" | agent mode: "hybrid"/"cua" (vision) vs "dom" (default, no vision) | | page_extraction_llm=... | extract("...", schema, { model }) | | planner_llm=... | agent({ model, executionModel }) — model plans, executionModel runs the inner act/observe |


3. Detailed translations

3.1 The Agent (the central decision)

A browser-use Agent is a fully-agentic loop: the LLM decides every click. In Stagehand you choose how much of that to keep. Default to decomposition when the script's intent reveals a concrete sequence; fall back to agent() only for genuinely open-ended tasks. Always note the choice in the migration summary. (Full decision framework: determinism.md.)

Before — browser-use

from browser_use import Agent, ChatAnthropic

agent = Agent(
    task="Go to Hacker News and open the top story",
    llm=ChatAnthropic(model="claude-sonnet-4-6"),
)
history = await agent.run(max_steps=30)
print(history.final_result())

After — decomposed (preferred: deterministic, debuggable, cheaper)

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

const stagehand = new Stagehand({ env: "BROWSERBASE", model: "anthropic/claude-sonnet-4-6" });
await stagehand.init();

const page = stagehand.context.pages()[0];
await page.goto("https://news.ycombinator.com");
await stagehand.act("click the top story link");

await stagehand.close();

After — faithful agentic (when the flow is open-ended)

const stagehand = new Stagehand({ env: "BROWSERBASE", model: "anthropic/claude-sonnet-4-6" });
await stagehand.init();

const agent = stagehand.agent();
const result = await agent.execute({
  instruction: "Go to Hacker News and open the top story",
  maxSteps: 30,
});
console.log(result.message);

await stagehand.close();
3.2 Structured output → extract() with a zod schema

Pydantic models become zod schemas. v3 supports a top-level array schema (no wrapper object needed). Prefer extracting after navigating to the page deterministically.

Before

from pydantic import BaseModel
from browser_use import Agent, ChatOpenAI

class Story(BaseModel):
    title: str
    points: int

class Stories(BaseModel):
    stories: list[Story]

agent = Agent(
    task="Get the top 5 Hacker News stories with title and points",
    llm=ChatOpenAI(model="gpt-5"),
    output_model_schema=Stories,
)
history = await agent.run()
data = history.structured_output   # Stories instance

After

import { z } from "zod";

const page = stagehand.context.pages()[0];
await page.goto("https://news.ycombinator.com");

const stories = await stagehand.extract(
  "extract the top 5 stories with their title and points",
  z.array(z.object({
    title: z.string(),
    points: z.number(),
  })),
);
// stories is fully typed: { title: string; points: number }[]

Use .describe() on fields to steer the model, mirroring Pydantic Field(description=...):

z.object({ price: z.string().describe("price including the currency symbol") })
3.3 Sensitive data / login → variables

browser-use's sensitive_data keeps secrets out of the prompt by injecting placeholder keys. Stagehand's variables do the same: the %key% token is sent to the LLM, the real value is substituted locally and never leaves your machine.

Before

sensitive_data = {
    "https://example.com": {"x_user": "[email protected]", "x_pass": "s3cret"},
}
agent = Agent(
    task="Log into example.com with username x_user and password x_pass",
    llm=llm,
    sensitive_data=sensitive_data,
    use_vision=False,
    browser=Browser(allowed_domains=["https://*.example.com"]),
)
await agent.run()

After

const page = stagehand.context.pages()[0];
await page.goto("https://example.com/login");

await stagehand.act("type %username% into the email field", {
  variables: { username: process.env.APP_USER! },
});
await stagehand.act("type %password% into the password field", {
  variables: { password: process.env.APP_PASS! },
});
await stagehand.act("click the sign in button");

For repeat runs, prefer a Browserbase Context so you log in once and reuse the authenticated state (see §4) — this is the biggest reliability win in most migrations.

3.4 Browser configuration

Local (dev)

# browser-use
browser = Browser(browser_profile=BrowserProfile(headless=False))
// Stagehand
const stagehand = new Stagehand({
  env: "LOCAL",
  localBrowserLaunchOptions: { headless: false, viewport: { width: 1280, height: 720 } },
});

Browserbase (prod) — in browser-use you create the session yourself and pass cdp_url. In Stagehand, env: "BROWSERBASE" creates and manages the session for you.

# browser-use
bb = Browserbase(api_key=os.environ["BROWSERBASE_API_KEY"])
session = bb.sessions.create(project_id=os.environ["BROWSERBASE_PROJECT_ID"])
browser = Browser(browser_profile=BrowserProfile(cdp_url=session.connect_url))
// Stagehand — apiKey/projectId default to BROWSERBASE_API_KEY / BROWSERBASE_PROJECT_ID env vars
const stagehand = new Stagehand({ env: "BROWSERBASE" });
3.5 Custom actions (@tools.action)

Decide whether the action is a deterministic side-effect (just write TypeScript) or a capability you want the autonomous agent to choose (register a tool).

Before

from browser_use import Tools, ActionResult, BrowserSession

tools = Tools()

@tools.action("Save the current URL to a file")
async def save_url(browser_session: BrowserSession) -> ActionResult:
    url = await browser_session.get_current_page_url()
    with open("url.txt", "w") as f:
        f.write(url)
    return ActionResult(extracted_content=f"saved {url}")

agent = Agent(task="...", llm=llm, tools=tools)

After — option A: plain code (preferred for deterministic side-effects)

import { writeFile } from "node:fs/promises";

const page = stagehand.context.pages()[0];
const url = page.url();
await writeFile("url.txt", url);

After — option B: a tool the agent can call (uses the Vercel AI SDK tool(); requires experimental: true — see the experimental callout below). ai must be v5 ("ai": "^5.0.0"): Stagehand 3.6.x types tools as the v5 ToolSet (schema field inputSchema); the v4 tool() helper emits parameters and fails to type-check. If you can't pin v5, drop the tool() helper and use a plain object { description, inputSchema: zodSchema, execute } (satisfies the v5 ToolSet regardless of the resolved ai major).

import { tool } from "ai"; // add the "ai" package (Vercel AI SDK) to your deps
import { z } from "zod";
import { writeFile } from "node:fs/promises";

const stagehand = new Stagehand({
  env: "BROWSERBASE",
  experimental: true, // REQUIRED for agent `tools` (throws ExperimentalNotConfiguredError otherwise)
  model: "anthropic/claude-sonnet-4-6",
});
await stagehand.init();

const page = stagehand.context.pages()[0];

const agent = stagehand.agent({
  tools: {
    saveUrl: tool({
      description: "Save the current page URL to a file",
      // The original reads the URL from `browser_session`, NOT the LLM. So take
      // no model args and close over `page` — otherwise the model can pass a
      // guessed/hallucinated URL. (Only put a field in inputSchema when the value
      // genuinely has to come from the agent's reasoning.)
      inputSchema: z.object({}),
      execute: async () => {
        const url = page.url();
        await writeFile("url.txt", url);
        return `saved ${url}`;
      },
    }),
  },
});
await agent.execute("...");

Custom-action gaps (flag for human review — no clean 1:1):

  • Injected special params. browser-use auto-wires browser_session, cdp_client, page_extraction_llm, available_file_paths, etc. into an action by signature. Stagehand tools get none of that — instead close over what you need in the execute body (capture the stagehand instance / page, call stagehand.act/extract inside the tool).

  • domains=[...] per-action filtering — no equivalent; gate inside execute with a page.url() host check (same pattern as the allowed_domains gap in §5).

  • terminates_sequence=True — no equivalent control-flow flag.

3.6 Secondary models
browser-useStagehand v3
page_extraction_llm=ChatOpenAI(model="gpt-5-mini")extract("...", schema, { model: "openai/gpt-5-mini" })
planner_llm=... + main llm=...agent({ model: "<planner>", executionModel: "<cheaper inner model>" })
3.7 MCP integration

browser-use has MCP in two directions — map only the client direction.

(a) browser-use as MCP client (consumes an external MCP server) → Stagehand agent({ integrations }) (requires experimental: true). Stagehand accepts either a URL string or an MCP Client from connectToMCPServer({ command, args, env }), so both browser-use transports map:

Before — browser-use (MCPClient, stdio/local server)

from browser_use.mcp.client import MCPClient
from browser_use import Tools, Agent, ChatOpenAI

tools = Tools()
mcp = MCPClient(server_name="filesystem", command="npx",
                args=["-y", "@modelcontextprotocol/server-filesystem", "/tmp"])
await mcp.connect()
await mcp.register_to_tools(tools)              # external MCP tools become agent actions
agent = Agent(task="...", llm=ChatOpenAI(model="gpt-4o"), tools=tools)

After — Stagehand

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

const stagehand = new Stagehand({
  env: "BROWSERBASE",
  experimental: true, // REQUIRED for `integrations`
  model: "openai/gpt-4.1-mini",
});
await stagehand.init();

// local/stdio MCP server -> Client instance:
const fsClient = await connectToMCPServer({
  command: "npx",
  args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"],
});
const agent = stagehand.agent({ integrations: [fsClient] });
// remote MCP server -> just the URL: integrations: ["https://mcp.example.com/mcp?key=..."]
  • register_to_tools(..., tool_filter=[...], prefix="...") has no equivalent — Stagehand attaches all of the server's tools; flag if the source filtered/renamed them.

(b) browser-use as MCP server (uvx browser-use --mcp, exposing the browser as MCP tools to Claude Desktop etc.) → no Stagehand equivalent. This is not a script migration — flag it as out of scope rather than converting it.

For both MCP-client transports, dispose the connection in finally: an MCP Client exposes .close() (the analog of browser-use's await mcp.disconnect()).

⚠️ Known runtime bug (Stagehand 3.6.0, verified live): passing a Client instance (i.e. a local/stdio server via connectToMCPServer) into integrations throws TypeError: Converting circular structure to JSON before the agent runs — agent() does JSON.stringify(options.integrations) for event logging and the Client object is circular (reproduced with two different MCP servers). A plain URL-string integration is unaffected, so prefer remote/URL MCP until this is fixed upstream; for a stdio-only server, flag that the migration is correct but blocked by this Stagehand bug.

3.8 Real-world patterns (embedded code, vision, legacy result handling)

Real browser-use lives inside apps, not clean main() scripts. Handle these explicitly:

  • Embedded / wrapped code (a LangChain BaseTool, FastAPI route, Celery/queue task, an Executor class). Convert only the browser-use surface — Agent(...) construction, agent.run(), result handling, custom tools — and preserve the surrounding app glue (the class, decorators, routing, logging) as-is. The init()/close()-in-main() skeleton and "page via context.pages()[0]" checklist items don't apply verbatim to a wrapper; adapt them to the wrapper's lifecycle.

  • Sync-over-async. Legacy embedded code often drives the async agent.run() from a sync method via asyncio.new_event_loop(). There's no faithful Node equivalent — the converted method becomes async and callers must await it. Note this in the migration summary.

  • Long-lived stateful executors. A persistent mutable self.agent (add_new_task, pause/resume, mid-run controller swaps, EventBus re-init) has no Stagehand analog — Stagehand agents aren't long-lived mutable objects. Model continuity with the stateless messages array (AgentResult.messages → next execute({ messages })) and rebuild tools per agent() call. pause/resume ≈ AbortSignal; rerun_history(plan) ≈ observe→act caching, not a 1:1. Flag these.

  • Vision intent. If the source LLM is a vision model (vl_/ChatVL-style, use_vision=True) but you map it to a text model string, you silently drop vision. Preserve the intent with agent mode: "hybrid" or "cua" (both need experimental: true), or flag it.

  • Legacy result API. Besides history.final_result() (method), legacy code uses the attribute form result.final_result and isinstance(result, AgentHistoryList) coercion → all map to result.message / result.output from the Stagehand agent result.


4. Browserbase platform features

Everything you could set on a raw Browserbase session is reachable through browserbaseSessionCreateParams (it is literally Browserbase's SessionCreateParams).

Needbrowser-use todayStagehand v3
Persistent auth / cookiesstorage_state / user_data_dirContext: browserSettings.context: { id, persist: true }
Proxiesprofile proxy / BB session proxiesproxies: true (or array form for geo/domain rules)
Stealth / fingerprinting(mostly via BB session)browserSettings.advancedStealth: true (Scale plan); Verified Sessions
Captcha solvingBB sessionbrowserSettings.solveCaptchas: true (on by default)
Ad blocking—browserSettings.blockAds: true
Region—`region: "us-east-1"
Keep session alivekeep_alive=TruekeepAlive: true
Downloads—bb.sessions.downloads.list(id) via @browserbasehq/sdk
const stagehand = new Stagehand({
  env: "BROWSERBASE",
  browserbaseSessionCreateParams: {
    projectId: process.env.BROWSERBASE_PROJECT_ID!,
    region: "us-east-1",
    proxies: true,
    keepAlive: true,
    browserSettings: {
      blockAds: true,
      solveCaptchas: true,
      context: { id: process.env.BB_CONTEXT_ID!, persist: true },
    },
  },
});

5. Gaps (no clean equivalent)

Call these out explicitly in the migration summary — they are guardrails or behaviors that do not transfer 1:1.

  • allowed_domains — browser-use can hard-block navigation off an allow-list. Stagehand has no built-in domain firewall. Mitigations: (a) constrain the autonomous agent via systemPrompt ("only operate within example.com"); (b) use Browserbase proxy domain rules; (c) enforce in your own code — check page.url() before acting. If the source relied on allowed_domains as a security boundary (it pairs with sensitive_data), flag it as needs human review.

  • Per-step thinking / use_thinking / flash_mode — browser-use exposes loop-level reasoning knobs. In Stagehand, decomposed act/extract calls are already targeted; for speed, use a fast model (google/gemini-2.5-flash) and decomposition rather than a "flash mode" flag.

  • max_actions_per_step — no equivalent; decomposition makes each action explicit instead.

  • initial_actions — there is no special field; it simply becomes ordinary code that runs before your first AI call.


6. Legacy browser-use patterns → current (normalize before translating)

LegacyCurrent browser-use
Browser(config=BrowserConfig(...))Browser(browser_profile=BrowserProfile(...)) or kwargs on Browser(...)
BrowserContextgone — folded into BrowserSession
Controller() + @controller.action ; controller=Tools() + @tools.action ; tools=
custom-action param browser: Browserbrowser_session: BrowserSession
cdp_url inside BrowserConfigcdp_url on Browser / BrowserProfile

Version notes (read before translating)

  • Stagehand v3 moved the AI methods onto the instance. It is stagehand.act(...), stagehand.extract(...), stagehand.observe(...) — not page.act(...). The page object (for goto, locators, etc.) is stagehand.context.pages()[0]. Most v2 examples online still show page.act() / stagehand.page — do not copy them verbatim.

  • Models are "provider/model" strings (e.g. "anthropic/claude-sonnet-4-6"), set via the model constructor field. Prefer the string form; the object form { modelName, ...clientOptions } still exists for passing client options, but the bare string is the idiomatic v3 path.

  • Page settling: use page.waitForLoadState("domcontentloaded") / "load", not "networkidle" (it times out on Google/analytics/long-poll pages).

  • Agent output schema requires experimental: true on the constructor and must be a zod object (not a top-level array). See the "Agent output gotchas" callout in §2; prefer the agent-then-extract pattern to avoid experimental mode.

  • Caching: cacheDir (local) and serverCache (Browserbase, default on) replace v2's enableCaching. domSettleTimeoutMs → domSettleTimeout.

  • zod is a peer dependency (v3 or v4): the consuming project must npm install zod.

  • Always confirm exact signatures against the installed version: https://docs.stagehand.dev/v3.

  • browser-use model strings move fast; the class names and model= parameter are stable, the exact model ids are not. Pin them per the team's installed version.

Requirements and compatibility

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta

“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.

Needs: Declares allowed tools, Needs an MCP server, Needs an API key

The author says: The skill itself uses only Read/Write/Edit/Grep/Bash — no install step. The Stagehand code it generates needs Node 18+, `@browserbasehq/stagehand` (v3) and `zod`, plus `BROWSERBASE_API_KEY` / `BROWSERBASE_PROJECT_ID` and a model-provider key (e.g. `ANTHROPIC_API_KEY`) to run. The optional trace-assisted path uses the Browserbase SDK or the sibling `browser-trace` skill.

Security

No issues found by our static scan· No issues found

A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.

Attribution and licence

Published by
Browserbase
Publisher
Browserbase (Official)
Source
browserbase/skills · skills/browser-use-to-stagehand · commit 6811ca3
Licence
MIT License (redistribution allowed)
  • ATTRIBUTION.md (written by AutoGineer)
Synced

AutoGineer is not affiliated with or endorsed by Browserbase or Browserbase.

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download browser-use-to-stagehand.zip