Browser Use To Stagehand
BrowserbaseOfficialPublished by Browserbase
- Skill
- Workflow
- Software development
- MIT
- No issues found
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
Download the skill as a ZIP:
https://mp-staging.autogineer.com/agent-skills/browserbase/browser-use-to-stagehand/downloadUnzip 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/stagehand3.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:
Stagehand v3: https://docs.stagehand.dev/v3 · installed types:
node_modules/@browserbasehq/stagehandBrowserbase: https://docs.browserbase.com
browser-use: https://docs.browser-use.com
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 chooseagent()vsact/extract/observevs cachedobserve→act. The decision tree. Read this when deciding how to translate anAgent(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, amcpServersconfig) 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_schemaPydantic models.Secrets —
sensitive_data, env-var usage, login flows.Guardrails —
allowed_domains,max_steps.Custom actions —
@tools.action/Controllerfunctions, 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 replayact(action)(no LLM call).Reading data →
extract("…", zodSchema).Genuinely open-ended → keep
stagehand.agent().execute(...)(tightened withmaxSteps/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";andimport { 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(...)— neverpage.act(...).Set the model as a
"provider/model"string.Default to
env: "BROWSERBASE"; showenv: "LOCAL"as the dev option.Pass secrets via
variablesandprocess.env, never hardcoded.await stagehand.init()at the start,await stagehand.close()in afinally.
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_domainsguardrails, 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 agenttool. Pin v5, not v4 — Stagehand 3.6.x bundlesaiv5 and typesagent({ tools })as the v5ToolSet, where a tool's schema field isinputSchema. The v4tool()helper emitsparametersinstead and will fail to type-check against Stagehand's v5ToolSet. If you can't control the hoistedaiversion, skip thetool()helper and pass a plain object{ description, inputSchema: zodSchema, execute }— it satisfies the v5ToolSetregardless of whichaimajor resolves.
.env
BROWSERBASE_API_KEY=...
BROWSERBASE_PROJECT_ID=...
ANTHROPIC_API_KEY=... # or the provider matching your model stringindex.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.[ ]
extractuses a zod schema;zodis in dependencies.[ ] Secrets use
variables+process.env; nothing hardcoded.[ ]
init()/close()present;close()infinally.[ ] 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 withpage.gotoand cache repeatable steps viaobserve→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
- ATTRIBUTION.mdAdded by AutoGineer918 B
- EXAMPLES.md7.1 KiB
- LICENSE.txt1.0 KiB
- SKILL.md11.2 KiB
- references/api-mapping.md24.2 KiB
- references/determinism.md7.8 KiB
- references/guide.md12.1 KiB
- references/prompt.md9.3 KiB
- references/trace-assisted.md5.7 KiB
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.
| Variant | Tell-tale imports / calls | Notes |
|---|---|---|
| Legacy (pre-0.12) | from browser_use import Browser, BrowserConfig; Browser(config=BrowserConfig(...)); BrowserContext; Controller(), @controller.action | Deprecated. 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.action | What 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, ChatOpenAIsurface is still the default in 0.13.x —ChatBrowserUseis 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 importsChatBrowserUse; require a literalbrowser_use.betaimport. 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):
Browseris now an alias forBrowserSession— same class. OldBrowserContextis gone (folded into the session).Controlleris a backwards-compat alias forTools;@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=MyPydanticModel | Preferred: 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_output | typed extract(...) return, or result.output from agent().execute({ output }) (gotchas below) |
Agent
outputgotchas (both bite at runtime, not compile time — verified on 3.6.x):
Requires experimental mode.
agent().execute({ output })throwsExperimentalNotConfiguredErrorunless the Stagehand constructor setsexperimental: 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 —outputschema, customtools(§3.5), and MCPintegrations(§3.7) all requireexperimental: 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.Must be a zod object (
output?: StagehandZodObject) — unlikeextract, a top-levelz.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 separatestagehand.extract("...", zodSchema)on the final page (or overresult.message). This keeps the managed API path and letsextractuse 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 LOCALlocalBrowserLaunchOptions.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"| agentmode: "hybrid"/"cua"(vision) vs"dom"(default, no vision) | |page_extraction_llm=...|extract("...", schema, { model })| |planner_llm=...|agent({ model, executionModel })—modelplans,executionModelruns 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 instanceAfter
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 theexecutebody (capture thestagehandinstance /page, callstagehand.act/extractinside the tool).domains=[...]per-action filtering — no equivalent; gate insideexecutewith apage.url()host check (same pattern as theallowed_domainsgap in §5).terminates_sequence=True— no equivalent control-flow flag.
3.6 Secondary models
| browser-use | Stagehand 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
Clientinstance (i.e. a local/stdio server viaconnectToMCPServer) intointegrationsthrowsTypeError: Converting circular structure to JSONbefore the agent runs —agent()doesJSON.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, anExecutorclass). 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. Theinit()/close()-in-main()skeleton and "page viacontext.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 viaasyncio.new_event_loop(). There's no faithful Node equivalent — the converted method becomesasyncand callers mustawaitit. 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 statelessmessagesarray (AgentResult.messages→ nextexecute({ messages })) and rebuild tools peragent()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 agentmode: "hybrid"or"cua"(both needexperimental: true), or flag it.Legacy result API. Besides
history.final_result()(method), legacy code uses the attribute formresult.final_resultandisinstance(result, AgentHistoryList)coercion → all map toresult.message/result.outputfrom 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).
| Need | browser-use today | Stagehand v3 |
|---|---|---|
| Persistent auth / cookies | storage_state / user_data_dir | Context: browserSettings.context: { id, persist: true } |
| Proxies | profile proxy / BB session proxies | proxies: true (or array form for geo/domain rules) |
| Stealth / fingerprinting | (mostly via BB session) | browserSettings.advancedStealth: true (Scale plan); Verified Sessions |
| Captcha solving | BB session | browserSettings.solveCaptchas: true (on by default) |
| Ad blocking | — | browserSettings.blockAds: true |
| Region | — | `region: "us-east-1" |
| Keep session alive | keep_alive=True | keepAlive: 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 viasystemPrompt("only operate within example.com"); (b) use Browserbase proxy domain rules; (c) enforce in your own code — checkpage.url()before acting. If the source relied onallowed_domainsas a security boundary (it pairs withsensitive_data), flag it as needs human review.Per-step thinking /
use_thinking/flash_mode— browser-use exposes loop-level reasoning knobs. In Stagehand, decomposedact/extractcalls 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)
| Legacy | Current browser-use |
|---|---|
Browser(config=BrowserConfig(...)) | Browser(browser_profile=BrowserProfile(...)) or kwargs on Browser(...) |
BrowserContext | gone — folded into BrowserSession |
Controller() + @controller.action ; controller= | Tools() + @tools.action ; tools= |
custom-action param browser: Browser | browser_session: BrowserSession |
cdp_url inside BrowserConfig | cdp_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(...)— notpage.act(...). The page object (forgoto, locators, etc.) isstagehand.context.pages()[0]. Most v2 examples online still showpage.act()/stagehand.page— do not copy them verbatim.Models are
"provider/model"strings (e.g."anthropic/claude-sonnet-4-6"), set via themodelconstructor 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
outputschema requiresexperimental: trueon the constructor and must be a zod object (not a top-level array). See the "Agentoutputgotchas" callout in §2; prefer the agent-then-extractpattern to avoid experimental mode.Caching:
cacheDir(local) andserverCache(Browserbase, default on) replace v2'senableCaching.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.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download browser-use-to-stagehand.zip