Skip to main content
Browser Use hands a task to an autonomous agent: you write Agent(task="...", llm=...), call run(), and the agent picks each action. Stagehand v4 has no equivalent object. Most of this migration hinges on these principles:
  1. There is no Agent. Nothing in v4 takes a whole task and drives the browser for you.
  2. v4 is a browser SDK plus three model-backed steps. act(), extract(), and observe() take natural-language instructions. Everything else is ordinary browser control.
Porting a flow means writing out the steps the agent used to infer, and deciding for each one whether it needs a model or a selector.
Browser Use is Python-first, so this guide leads with Python. Stagehand behaves the same way in TypeScript; the SDK reference carries each language’s naming.

Why there’s no Agent

Agent(task=...).run() was built for a world where a model couldn’t be trusted with the browser on its own, so the framework wrapped it in a loop, showed it the page each step, and asked it to pick one action at a time. Every step was an inference call, whether the page needed judgement or not. Many of those steps do not need a model: navigating to a URL, clicking a button with a stable selector, reading a table. v4 exposes discrete tools and leaves the control flow to you. Two approaches replace the agent:
  • Code mode puts the model in front of the run, not inside it. A coding assistant writes a Stagehand script once; you run that script every time after. Browserbase recommends starting here.
  • Tool calling keeps a model in the loop at runtime, the way Browser Use does, but drives the browser through the full Stagehand API as its tools instead of one broad task string.
Either way, act(), extract(), and observe() stay in your toolbox for the steps that genuinely need a model. You just stop handing a model the entire task.

Hello world, side by side

The smallest Browser Use program and its v4 shape:
The shape of the migration is already visible:
  • A browser factory (browserbase.launch() or local_browser.launch()) replaces the implicit browser inside Agent, and Stagehand.create() attaches the runtime to it.
  • The task string is gone. You write the steps.
  • Where Browser Use would have spent a model call reading the page, a page.locator() does it for free. Spend act() and extract() only where the page needs judgement.
Stagehand reads no environment variables of its own. Browser Use auto-loads .env and picks up OPENAI_API_KEY, BROWSER_USE_API_KEY, and friends. In v4 you pass every key explicitly: the Browserbase API key to the factory, and any model key in the model option. load_dotenv() still works to get values into os.environ; nothing reads them for you.

Code mode

Ask a coding assistant to write the Stagehand script, then run the script. The model writes the code once, instead of driving the browser on every run. You get ordinary code: reviewable, diffable, and free of per-step inference. When a site changes, re-run the assistant on the step that broke. Start with AI rules. Those rule files keep generated code on the v4 API instead of the older Stagehand and Browser Use patterns in a model’s training data. Here’s a prompt that produces a working script:
What comes back should read like the script you’d have written yourself:
Generated code should use page.locator() and page.goto() wherever a selector is stable, and spend a model call only where the page needs judgement. A Browser Use agent couldn’t make that split, because every step it ran was an inference call.

Tool calling

To keep a model in the loop at runtime, the way Browser Use does, give it the whole Stagehand surface rather than one task string. Each method maps to one tool with a narrow contract, so a step names a specific browser operation instead of routing everything through one sentence of English and hoping the agent picks the right action. Browser Use also lets you register custom tools with @tools.action(...) (formerly @controller.action(...)). Those port directly: each becomes one function in the tool set below. Expose the real API: page.snapshot() anchors the loop the way Browser Use’s page state did. It returns formattedTree, the accessibility tree, plus an xpathMap, so the model reads real page structure and hands back a selector you can drive deterministically.
Escalate on observe(), never on act(). A failed act() may already have clicked, submitted, or paid before the error surfaced, so retrying it can repeat the side effect. observe() only plans, so retrying it is free. Browser Use’s max_failures retry loop had the same hazard; keep the retry on the planning step. Cost optimization applies the same idea to model escalation.

Let a coding assistant do the rest

Most of the mechanical mapping below is exactly what an assistant is good at. Point it at this page instead of retyping the rules:
Set up AI rules first, so the assistant stays on the v4 API instead of the Stagehand and Browser Use patterns in its training data. Then work through the sections below for anything it missed.
  1. Get one script constructing and closing cleanly on v4, before porting any behavior. Launch a browser, open a page, close both handles.
  2. Replace Agent(task=...).run() with code mode or tool calling. This is the real work, and everything else is mechanical.
  3. Convert the deterministic steps to page.locator() and page.goto(), keeping act() and extract() only where the page needs judgement.
  4. Move output_model_schema to an extract() call with a schema.
  5. Move sensitive_data to variables on act().
  6. Turn on server-side caching once the flow is stable.

Breaking changes

Initialization and teardown

Browser Use constructs a browser inside the agent and closes it for you. v4 separates the browser from the runtime, and you close both:
Use local_browser.launch() for a browser on your machine, browserbase.launch({ apiKey }) for a hosted one, and local_browser.connect({ cdpUrl }) or browserbase.connect({ apiKey, sessionId }) to attach to one that’s already running. browserbase.launch() is what enables server-side caching and the Model Gateway. Stagehand closes only the browsers it launched, so stagehand.close() leaves the browser running and you call browser.close() yourself. See browser configuration.

The task string becomes explicit steps

There’s no direct diff here, because a task string has no single replacement. That’s the whole migration: what the agent inferred each step, you now write out. Read code mode and tool calling, pick one, and translate the intent of the task into steps.

Models

Browser Use selects a provider by the chat class you construct (ChatOpenAI, ChatAnthropic, ChatGoogle, ChatBrowserUse). v4 takes one model object, and the model name always carries a provider prefix:
On a Browserbase browser you can omit model entirely and the Model Gateway picks one for you, which is the closest analogue to ChatBrowserUse(). Pass the same shape to a single act(), extract(), or observe() call to override it there. Browser Use’s page_extraction_llm (a separate model for extraction) maps to passing model on the extract() call. See models.

Structured output

Browser Use validates the final result against output_model_schema on the agent. v4 puts the schema on the extract() call that reads the data, and returns it typed:
Calling extract() with no schema returns { extraction: string }. See extract.

Sensitive data

Both frameworks keep secrets out of the model’s context. Browser Use uses a sensitive_data dict of placeholder-to-value; v4 uses variables with %name% placeholders in the instruction, on act() and observe():
Stagehand exposes only the variable names to the model and substitutes the real values locally. One exception: with server-side caching on, variable values travel to the cache service, so turn cache off for calls that carry credentials. See act. For Browser Use’s TOTP support (sensitive_data keys ending in bu_2fa_code), generate the code in your own script and pass it as a variable; v4 has no built-in 2FA step.

Custom tools

Browser Use’s @tools.action(...) / @controller.action(...) decorators register functions the agent can call. In v4 there’s no agent to register them with, so each custom action becomes an ordinary function you call directly in code mode, or one entry in your tool-calling tool set. The function body ports as-is; only the registration goes away.

Browser configuration

Browser(...) (aliased BrowserSession) options map onto the browser factory. The common ones: Browser Use’s @sandbox decorator runs the agent next to the browser on cloud infrastructure. The v4 equivalent is a Browserbase browser: browserbase.launch() gives you the hosted session, and the live view, recording, and network detail replace @sandbox’s on_browser_created / live_url and on_log callbacks.

Quick reference

Troubleshooting

ImportError: cannot import name 'Agent'. There is no Agent in v4. Replace Agent(task=...).run() with code mode or tool calling. Nothing reads my API key. Stagehand reads no environment variables. Pass the Browserbase key to browserbase.launch() and any model key in the model option. load_dotenv() only populates os.environ; you still pass the values in. My script has no history object to read results from. There’s no run history. The value you’d have read from history.final_result() or history.structured_output is the return of your last extract() call, on .data. My custom @tools.action function has nowhere to register. Call it directly in code mode, or add it to your tool set for tool calling. Only the decorator goes away; the function body is unchanged. A retried step repeats a side effect. You’re retrying act(), the same hazard Browser Use’s max_failures loop had. Retry observe() instead and pass the resulting action to act() once. My generated script uses Agent or old Stagehand APIs. The assistant is drawing on Browser Use and older Stagehand patterns in its training data. Install the rule files from AI rules.

Next steps

AI rules

Set your coding assistant up to write v4 code

Act

Perform one action, or replay an observed one

Extract

Pull typed data, the home for structured output

Caching

Cut inference out of a stable flow