CodeWorlds
Back to collections
Guide13 min readCodeWorlds Team

Stagehand, browser automation driven by an LLM

Stagehand 4.0.2 turns sentences into browser actions. MIT license, local mode without a Browserbase account, server-side cache and the real cost of two bills.

Stagehand, browser automation driven by an LLM

Stagehand is a browser control library where you describe an action in a sentence and a model turns it into clicks and typing. Version 4.0.2, released on 20 August 2026, is MIT licensed and runs locally, but action caching and automatic model selection work only on the paid Browserbase infrastructure.

What Stagehand is and what version 4 changed

The library gives you three model-driven methods plus a full deterministic browser driver next to them. act performs a single action described in a sentence, observe returns a list of clickable elements together with ready selectors, and extract pulls structured data according to a Zod or Pydantic schema. Beside them sits the Page class with goto, click, type, keyPress, screenshot, snapshot, evaluate, waitForSelector and locator, plus the Locator class with click, fill, innerText, selectOption, setInputFiles, nth and first.

Version 4 changed the foundation. Package 3.7.2 declared playwright-core at ^1.55.1 as an optional peer dependency, next to puppeteer-core and patchright-core. Package 4.0.2 has none of them. It ships its own Chrome DevTools Protocol client and a browser extension instead: the tarball contains a dist/extension directory with a service-worker.js weighing about two megabytes and a manifest.json version three requesting the debugger, offscreen, scripting and tabs permissions plus access to all URLs.

This distinction matters in practice. The method names resemble Playwright, but this is not Playwright, it is a separate implementation with a similar interface. Plugins, reporters, the test runner's expect and the rest of the ecosystem do not carry over automatically. Model inference happens inside the extension, which calls api.openai.com, api.anthropic.com, generativelanguage.googleapis.com, api.groq.com or api.cerebras.ai itself, depending on the chosen provider.

Version, license and what is in the package

I checked the license in three places and the answer is consistent. The LICENSE file in the browserbase/stagehand repository holds the MIT text with the line Copyright (c) 2024 Browserbase Inc. The license field in the npm registry for @browserbasehq/stagehand reads MIT. The published stagehand-4.0.2.tgz tarball has sixteen files, including package/LICENSE with the same text, and the code is real: dist/index.mjs weighs 192 kilobytes, the dist/index.d.mts type declarations another 244 kilobytes, and the whole thing unpacks to roughly 3.3 megabytes. This is no stub and no empty placeholder.

One small divergence concerns the Python package. The stagehand-4.0.2-py3-none-any.whl wheel declares License-Expression: MIT in its metadata and contains forty files of real code, but its dist-info directory has no license text file, only METADATA, WHEEL and RECORD. If your dependency audit requires a physical license file inside the artifact, the Python build needs one pulled from the repository. The README adds that Stagehand is a trademark of Browserbase, which limits use of the brand itself, not the code.

The project looks active. The repository has around 24 thousand stars, releases 4.0.0, 4.0.1 and 4.0.2 landed on 10, 14 and 20 August 2026 respectively, and the 3.7.2 branch got a release on the same day as 4.0.2. The freshness of the major is a risk in itself: I am writing about a library whose major version is twelve days old, so plenty of examples online and in model answers still describe the version 3 interface.

The technical requirements can surprise you. The engines field asks for Node 22.18.0 or newer, and exports only exposes the ESM build, so require will not work. The zod dependency is pinned to exactly 4.4.3, with no range.

Local or Browserbase

The library runs without a Browserbase account, and that answers the most important question about vendor lock-in. The localBrowser.launch() factory starts Chrome on your machine, and localBrowser.connect({ cdpUrl }) attaches to a browser you already run at the given address. The launch options include headless, executablePath, userDataDir, preserveUserDataDir, proxy, viewport, downloadsPath and chromiumSandbox, among others. No Browserbase key is required anywhere in that path.

Three things stop working outside the vendor's cloud, and all three matter. First the cache: the context builder in the extension code returns undefined when apiKey or sessionId is missing, and a local browser has no Browserbase session id. The cache option becomes dead weight and every call pays the full inference price. Second the Model Gateway, the mode without a model field in which the vendor picks a model for each call. The documentation states plainly that it needs a hosted browser, because there is nothing to bill and authorize against. Third the whole infrastructure layer: proxies, stealth mode, session recordings, region choice and scaling parallel sessions.

Locally you therefore have to supply your own model key in model.apiKey or provide your own generate function that talks to any provider, including a model running on your own hardware. The supported model name prefixes are openai/, anthropic/, google/, groq/ and cerebras/, so an account at OpenAI or access to Claude covers it.

Three AI methods and the way back to selectors

The simplest local example looks like this.

Code
Bash
node --version   # 22.18.0 or newer required
npm install @browserbasehq/stagehand zod
export OPENAI_API_KEY="sk-..."
Code
TypeScript
import { localBrowser, Stagehand } from '@browserbasehq/stagehand'
import { z } from 'zod/v4'

const browser = await localBrowser.launch({ headless: false })

const stagehand = await Stagehand.create({
  browser,
  model: {
    modelName: 'openai/gpt-5.4-mini',
    apiKey: process.env.OPENAI_API_KEY
  },
  selfHeal: true,
  domSettleTimeoutMs: 3000
})

const page = await browser.context.activePage()
await page.goto('https://news.ycombinator.com')

const { data, metadata } = await stagehand.extract(
  'list the title and score of the first three entries',
  z.object({
    items: z.array(z.object({ title: z.string(), points: z.number() }))
  }),
  { screenshot: false }
)

console.log(data.items, metadata.usage.inputTokens, metadata.usage.inferenceTimeMs)
await stagehand.close()
await browser.close()

The more interesting pattern is the one where the model works once and then steps aside. observe returns an array of objects with selector, description, method and arguments fields. You can store the selector on your side and use it directly on later runs, with no model call at all.

Code
TypeScript
const { data: actions } = await stagehand.observe('find the login button')

// deterministic path: click by selector, zero tokens
await page.locator(actions[0].selector).click()

// model path, for when the page changed and the selector no longer matches
await stagehand.act('click the login button', {
  timeout: 20000,
  variables: {
    login: { value: process.env.APP_LOGIN!, description: 'user name' }
  }
})

const metrics = await stagehand.metrics()
console.log(metrics.actPromptTokens, metrics.totalInferenceTimeMs)

The variables field exists so that you do not paste passwords into the instruction text, which goes to the model and becomes part of the cache key. The locator and ignoreLocators options narrow the part of the page the model works on, which shortens the context and lowers the bill.

The server-side cache

A caching mechanism exists, but it is not what it looks like at first glance, as the caching documentation confirms. The cache does not live on disk next to your project, it lives in a service at api.stagehand.browserbase.com, with separate endpoints for the us-west-2, us-east-1, eu-central-1 and ap-southeast-1 regions. Requests go to the /cache/get and /cache/set routes with an x-bb-api-key header, and the project is resolved from the session id.

The key is built from the method, the page URL, the accessibility tree collected over CDP and the options you passed. Model configuration is deliberately excluded from the key, so switching models does not invalidate stored entries. On top of that sits a hit threshold: the threshold field says how many times the vendor must see an identical result before it starts serving it. A value of 1 means a hit on the second run, a higher value delays the point at which a result counts as stable.

Every act, observe and extract result carries a metadata.cache object with status HIT, MISS or DISABLED. A hit gives you count, threshold and tokensSaved, a miss gives you missReason, including the distinction between replay_failed, when an entry was found but could not be replayed, and read_failed, when the cache request itself failed. The DISABLED status is what you always see when working locally.

Code
TypeScript
import { browserbase, Stagehand } from '@browserbasehq/stagehand'

const browser = await browserbase.launch({
  apiKey: process.env.BROWSERBASE_API_KEY!,
  region: 'eu-central-1'
})

const stagehand = await Stagehand.create({
  browser,
  cache: { threshold: 2 }
})

const result = await stagehand.act('click the first search result', {
  cache: { threshold: 1 }
})

if (result.metadata.cache.status === 'HIT') {
  console.log('tokens saved:', result.metadata.cache.tokensSaved?.totalTokens)
} else {
  console.log('miss reason:', result.metadata.cache.missReason)
}

Two bills: infrastructure and model

The first bill is Browserbase. The free plan gives three concurrent browsers, one browser hour, three agent runs, one thousand Search calls and one thousand Fetch calls, sessions capped at fifteen minutes, seven days of data retention and five dollars in tokens. The Developer plan costs 20 dollars per month and gives 25 concurrent browsers and one hundred hours, then 0.12 dollars per hour. The Startup plan is 99 dollars per month, one hundred concurrent browsers, five hundred hours, then 0.10 dollars per hour, and thirty days of retention. The Scale plan is priced individually.

The pricing page has no annual billing toggle and no yearly price, so twelve months of the Developer plan is simply 240 dollars, with no discount. I did find a discrepancy inside the pricing page itself: the Startup plan card quotes Fetch overage at 1 dollar per thousand calls, while the comparison table further down the same page quotes 0.50 dollars per thousand for the same plan. Both numbers are there today, so confirm the rate with the vendor before you build a budget on it.

The second bill is the model. Every act, observe and extract that misses the cache is a model call with the page accessibility tree in the context. Running locally you pay it every time, and you pay it separately, on the model provider's invoice. The Model Gateway merges both bills into one, at the cost of the browser having to live at the vendor. Before you cost out a scraping campaign, collect metrics() from a single run and multiply by the number of pages.

When natural language helps and when it hurts

A CSS selector is free, instant and deterministic: it either matches or it does not. A sentence in natural language costs a model call, takes hundreds of milliseconds or seconds, and on the next run may pick a different element because the page shifted or the model answered differently. That is not an implementation flaw, it is a property of the tool.

For a stable regression suite Stagehand is the wrong choice. A regression test has to produce the same result on every run, and a red light has to mean a bug in the application, not a whim of the model. That is what Playwright or Cypress are for, with the logic layer tested in Vitest. Stagehand pays off where the page belongs to someone else and changes without warning: scraping services with no API, filling forms in partner portals, pulling data from dashboards nobody versions.

CriterionStagehand 4.0.2browser-use 0.13.8Playwright 1.62.1
Controlsentence plus selectorstask for an agentselectors only
Engineown CDP client and extensionChromium over CDPown drivers
LicenseMITMITApache 2.0
LanguagesTypeScript, Python, GoPythonTypeScript, Python, Java, .NET
Cost per callmodel tokensmodel tokenszero
Repeatabilitydepends on model and cachedepends on modelfull
Action cacheserver-side, Browserbase onlynone built innot applicable
Local runyes, no vendor accountyesyes

The sensible hybrid setup looks like this: run observe once, commit the selectors to your repository, and keep act as the rescue path for when a stored selector stops matching. You then pay the model on change, not on every run. The comparison with browser-use comes down to the level of abstraction: there you describe the goal of the whole task and hand the agent control of the loop, here you drive step by step and can drop down to a raw selector at any moment.

Common mistakes

The first mistake is expecting cache: true to do anything with a local browser. It does not, and the only signal is the DISABLED status in the metadata, which nobody reads.

The second is putting act into a regression suite and being surprised that one run in twenty comes out different. The same effect comes from the Model Gateway with no pinned model: if the vendor picks a model per call, two runs can use different models.

The third is copying version 3 examples, where the methods hung off the page object. In version 4, act, observe and extract are methods on the Stagehand instance, and the page serves navigation and selectors.

The fourth is the environment: Node older than 22.18.0, or a CommonJS project. The package exports ESM only and require will simply blow up.

The fifth is passwords pasted straight into the instruction instead of into variables. The instruction goes to the model and becomes part of the cache key.

The sixth is ignoring context cost. Without locator or ignoreLocators the model receives the accessibility tree of the entire page, and you pay for it on every cache miss.

FAQ

Does Stagehand work without a Browserbase account?

Yes. localBrowser.launch() starts Chrome on your machine and localBrowser.connect({ cdpUrl }) attaches to a browser you already run. You then need your own model key or your own generate function. Without a vendor account you lose the cache, the Model Gateway, proxies, stealth mode and session recordings.

Will Stagehand replace Playwright in tests?

Not in regression tests. A model call carries no repeatability guarantee, and a test that turns red once every few dozen runs with no code change stops being useful. Stagehand fits exploratory flows and pages you do not control.

How much does one act call cost?

It depends on the model and the size of the page accessibility tree, so there is no single number. Measure it yourself: metadata.usage returns inputTokens, outputTokens, reasoningTokens and inferenceTimeMs for a single call, and stagehand.metrics() aggregates the same figures split across act, extract and observe.

How does Stagehand differ from browser-use?

In the level of control. browser-use takes a goal and drives the agent loop itself. Stagehand performs one action per call, returns selectors through observe, and lets you fall back to a deterministic page.locator() at any time. Stagehand also ships official SDKs for TypeScript, Python and Go.

How expensive is migrating from version 3 to 4?

The foundation changed, not just the names. The playwright-core dependency is gone, the AI methods moved from the page object to the Stagehand instance, the browser is created by a factory and passed into Stagehand.create, and the required Node version rose to 22.18.0. The 3.7.x branch still gets releases, so the migration does not have to happen at once.

Read next

We use cookies to enhance your experience on the site