Chapter 05 · Browser Use To Stagehand
Subchapter 5.3
references/guide.mdMarkdown12 KBView on GitHub
A guide for teams moving browser automation from browser-use (Python) to Stagehand (TypeScript) running on Browserbase. It focuses on mapping features and choosing the right level of determinism — not a rigid line-by-line transpile, because real migrations differ by team, tooling, and how much autonomy each workflow actually needs.
If you want an agent to do the rewriting for you, pair this guide with the /browser-use-to-stagehand skill,
which applies the mappings and determinism framework below to your actual scripts.
observe→act for stable, repeatable steps,
act/extract for the parts that vary, and a full agent() only when the path is open-ended.env: "BROWSERBASE" manages the
session for you, and unlocks Contexts (persistent auth), proxies, stealth, and observability.| browser-use | Stagehand | |
|---|---|---|
| Default control model | Fully agentic — LLM picks each action | You choose, per step, how much AI to use |
| Unit of work | A natural-language task | Primitives: act, extract, observe, agent |
| Page perception | Indexed DOM/accessibility tree (vision optional) | Targeted per-call; DOM by default, vision in agent hybrid/cua modes |
| Determinism | Hard — every run re-reasons | A dial: from cached, replayable actions to full agent |
| Best at | Open-ended exploration, prototyping | Production flows you want repeatable and cheap |
| Language | Python | TypeScript (also Python) |
Teams usually migrate because an agentic script that was perfect for a demo becomes expensive, flaky, or impossible to debug in production. The fix isn’t “a better agent” — it’s using AI only where the page is actually unpredictable and making everything else deterministic.
browser-use runs a perceive → decide → act loop. Each step it snapshots the page into an
indexed element tree, asks the LLM which action to take, executes it, and repeats until the task is
done or max_steps is hit. One Agent(task="…") can hide a dozen decisions.
Stagehand exposes four primitives you compose yourself:
act(instruction) — perform one action (“click the login button”). One LLM call.extract(instruction, schema) — pull structured (zod-typed) data off the page.observe(instruction) — resolve an instruction into concrete actions without doing them;
replay them later with no LLM call (the determinism trick).agent().execute(instruction) — the full autonomous loop, when you genuinely need it.Navigation to known URLs is just page.goto() on the Stagehand page (no AI), and you lock in
repeatable steps by caching an observe() result and replaying it with act(). The migration is
mostly deciding which primitive each browser-use step becomes.
The most-used mappings (see the skill’s
api-mapping.md for the exhaustive table and
before/after code):
| browser-use | Stagehand v3 / Browserbase |
|---|---|
Agent(task=…) + agent.run() | Decompose into act/extract/observe when the flow is known; else stagehand.agent().execute(…) |
llm=ChatAnthropic(model="claude-sonnet-4-6") | new Stagehand({ model: "anthropic/claude-sonnet-4-6" }) |
output_model_schema=PydanticModel | extract("…", zodSchema) |
history.final_result() / .structured_output | extract(...) return / result.output |
Browser() (local) | new Stagehand({ env: "LOCAL", localBrowserLaunchOptions }) |
Browser(cdp_url=session.connect_url) | new Stagehand({ env: "BROWSERBASE" }) |
sensitive_data={…} | act("…%key%…", { variables: { key } }) |
storage_state / user_data_dir | Browserbase Context (browserSettings.context: { id, persist: true }) |
| proxies / stealth / captcha | browserbaseSessionCreateParams (proxies, browserSettings.advancedStealth, solveCaptchas) |
@tools.action custom action | plain TS (deterministic), or agent({ tools }) (agent capability) |
max_steps | agent().execute({ maxSteps }) |
allowed_domains | ⚠️ no direct equivalent — see Gotchas |
Every browser-use step lands somewhere on this spectrum. Choosing well is the whole game. (The
skill’s determinism.md has the decision tree.)
| Level | Stagehand | Use when | Cost / determinism |
|---|---|---|---|
| 1. Autonomous | agent().execute("…") | Path is open-ended / unknown | Highest cost, lowest determinism (≈ browser-use) |
| 2. Per-step AI | act("…"), extract("…", schema) | Steps known, markup varies | One call/step; inspectable |
| 3. Observe → act | observe() then act(action) | Known steps you want to replay | act(action) makes no LLM call |
| 4. Self-heal + cache | selfHeal, cacheDir, serverCache | Production replay that tolerates DOM drift | Cheapest steady state |
| 5. Navigation (no AI) | page.goto(url), page.url() | Loading a known URL (element interactions are Levels 2–4) | No AI, no cost |
A healthy rewrite is a mix: page.goto for navigation + cached observe→act for the
repeatable skeleton + per-step act/extract for the variable bits + a Context for auth. Reserve
agent() for the one genuinely open-ended stretch, if any.
Example decomposition. browser-use:
Agent(task="Go to the store, search 'wireless mouse', add the cheapest to cart, checkout with my saved card")becomes:page.goto(store)(L5) →act("search 'wireless mouse'")(L2) →extractprices, pick the min in code,act("add to cart")(L2 + code) → checkout via a Browserbase Context so the card/session is already authenticated (L3/4). One opaque agent run → an inspectable pipeline.
You don’t have to rewrite everything at once. A low-risk sequence:
cdp_url=session.connect_url. You immediately gain observability,
proxies, and stealth, and you establish a behavior baseline. (Optional but recommended first
step — it de-risks everything after.)sessions.logs.list) to see the real navigations and network calls. Use the video
recording for human QA. See the skill’s
trace-assisted.md.page.goto,
repeatable steps to cached observe→act — the variable steps to act/extract, and only the
open-ended parts to agent(). Move auth to a Context, secrets to variables.selfHeal + caching, pin models, scope extracts with
selector, lock the viewport, wait for domcontentloaded (never networkidle) before AI snapshots.Before — browser-use (Python)
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="Go to Hacker News and return the top 5 stories with their title and points",
llm=ChatOpenAI(model="gpt-5"),
output_model_schema=Stories,
)
history = await agent.run()
print(history.structured_output)After — Stagehand v3 (TypeScript) on Browserbase
import "dotenv/config";
import { Stagehand } from "@browserbasehq/stagehand";
import { z } from "zod";
async function main() {
const stagehand = new Stagehand({ env: "BROWSERBASE", model: "openai/gpt-5" });
await stagehand.init();
try {
const page = stagehand.context.pages()[0];
await page.goto("https://news.ycombinator.com"); // deterministic
const stories = await stagehand.extract( // structured read
"extract the top 5 stories with their title and points",
z.array(z.object({ title: z.string(), points: z.number() })),
);
console.log(stories);
} finally {
await stagehand.close();
}
}
main().catch((err) => { console.error(err); process.exit(1); });The navigation that the agent used to “decide” is now an explicit page.goto, and the data read is
a single typed extract instead of a full agent loop — faster, cheaper, and deterministic. See
EXAMPLES.md for more before/after pairs (simple task, structured extraction, login).
These are often the real reason to migrate, beyond determinism:
All reachable from Stagehand via browserbaseSessionCreateParams.
stagehand.act/extract/observe),
not the page; get the page via stagehand.context.pages()[0]; models are "provider/model"
strings. Most examples online are v2 (page.act(), stagehand.page) — don’t copy them. See the
v2→v3 migration guide (opens in a new tab).browser_use.beta). Identify which the source uses before translating; class names differ
(Browser ≡ BrowserSession, Controller → Tools).allowed_domains has no Stagehand equivalent. browser-use can hard-block off-domain
navigation (and pairs it with sensitive_data as a security boundary). In Stagehand, constrain
the agent via systemPrompt, use Browserbase proxy domain rules, or check page.url() in code —
and treat it as a deliberate review item, not an automatic drop.agent(). That just reproduces browser-use’s
non-determinism. Decompose where the flow is known.variables + environment variables; never hardcode. Prefer Contexts for auth.Stagehand
Browserbase
browser-use