Stagehand AI Agent
The SDK to extract data and interact with any site on the web. Get started with Claude Code, Codex, Eve, Mastra, and more.
Stagehand is an open-source ai agent by browserbase, written mainly in TypeScript, with 25,894 GitHub stars and the MIT license. Its code was last updated Oct 30, 2026. The latest npm release is @browserbasehq/stagehand 4.4.0, published Nov 4, 2026, with 2,630,648 downloads last week.
AI that uses the browser like humans.
Sign in once, keep the session, and pull structured data out the other side.
import { localBrowser, Stagehand } from "@browserbasehq/stagehand";
import { z } from "zod/v4";
// Cookies persist in ./browser-data, so the next run starts already signed in
const browser = await localBrowser.launch({ userDataDir: "./browser-data" });
const stagehand = await Stagehand.create({
browser,
model: { modelName: "openai/gpt-5.4-mini", apiKey: process.env.OPENAI_API_KEY },
});
const [page] = await browser.context.pages();
await page.goto("https://app.example.com/login");
// observe() returns real selectors, so credentials never reach the model
const { data: email } = await stagehand.observe("find the email input");
const { data: password } = await stagehand.observe("find the password input");
await page.locator(email[0].selector).fill(process.env.APP_EMAIL!);
await page.locator(password[0].selector).fill(process.env.APP_PASSWORD!);
// act() self-heals when the site redesigns its form
await stagehand.act("click the sign in button");
await stagehand.act("open the billing page");
// extract() returns schema-validated data
const { data } = await stagehand.extract(
"extract every invoice in the table",
z.object({
invoices: z.array(z.object({ number: z.string(), amount: z.number(), paid: z.boolean() })),
}),
);
console.log(data.invoices);
await stagehand.close();
await browser.close();
<details>
<summary><b>Python</b></summary>
import asyncio
import os
from pydantic import BaseModel
from stagehand import Stagehand, local_browser
class Invoice(BaseModel):
number: str
amount: float
paid: bool
class Invoices(BaseModel):
invoices: list[Invoice]
async def main() -> None:
# Cookies persist in ./browser-data, so the next run starts already signed in
browser = await local_browser.launch(user_data_dir="./browser-data")
try:
stagehand = await Stagehand.create(
browser=browser,
model="openai/gpt-5.4-mini",
model_api_key=os.environ["OPENAI_API_KEY"],
)
try:
page = (await browser.context.pages())[0]
await page.goto("https://app.example.com/login")
# observe() returns real selectors, so credentials never reach the model
email = await stagehand.observe("find the email input")
password = await stagehand.observe("find the password input")
await page.locator(email.data[0].selector).fill(os.environ["APP_EMAIL"])
await page.locator(password.data[0].selector).fill(os.environ["APP_PASSWORD"])
# act() self-heals when the site redesigns its form
await stagehand.act("click the sign in button")
await stagehand.act("open the billing page")
# extract() returns schema-validated data
result = await stagehand.extract(
"extract every invoice in the table",
Invoices,
)
print(result.data.invoices)
finally:
await stagehand.close()
finally:
await browser.close()
asyncio.run(main())
</details>
<details>
<summary><b>Go</b></summary>
package main
import (
"context"
"errors"
"fmt"
"log"
"os"
stagehand "github.com/browserbase/stagehand/packages/sdk-go/v4"
)
type invoice struct {
Number string `json:"number"`
Amount float64 `json:"amount"`
Paid bool `json:"paid"`
}
type invoices struct {
Invoices []invoice `json:"invoices"`
}
func main() {
if err := run(context.Background()); err != nil {
log.Fatal(err)
}
}
func run(ctx context.Context) (err error) {
// Cookies persist in ./browser-data, so the next run starts already signed in
browser, err := stagehand.LaunchLocalBrowser(ctx, &stagehand.LocalBrowserLaunchOptions{
UserDataDir: "./browser-data",
})
if err != nil {
return err
}
defer func() { err = errors.Join(err, browser.Close(ctx)) }()
modelAPIKey := os.Getenv("OPENAI_API_KEY")
client, err := stagehand.Create(ctx, stagehand.CreateOptions{
Browser: browser,
Model: &stagehand.ModelConfig{
ModelName: "openai/gpt-5.4-mini",
APIKey: &modelAPIKey,
},
})
if err != nil {
return err
}
defer func() { err = errors.Join(err, client.Close(ctx)) }()
browserContext, err := browser.Context()
if err != nil {
return err
}
pages, err := browserContext.Pages(ctx)
if err != nil {
return err
}
page := pages[0]
if _, err := page.Goto(ctx, "https://app.example.com/login", nil); err != nil {
return err
}
// Observe returns real selectors, so credentials never reach the model
emailInstruction := "find the email input"
email, err := client.Observe(ctx, &emailInstruction, nil)
if err != nil {
return err
}
if err := page.Locator(email.Data[0].Selector).Fill(ctx, os.Getenv("APP_EMAIL")); err != nil {
return err
}
passwordInstruction := "find the password input"
password, err := client.Observe(ctx, &passwordInstruction, nil)
if err != nil {
return err
}
if err := page.Locator(password.Data[0].Selector).Fill(ctx, os.Getenv("APP_PASSWORD")); err != nil {
return err
}
// Act self-heals when the site redesigns its form
if _, err := client.Act(ctx, stagehand.ActInstruction("click the sign in button"), nil); err != nil {
return err
}
if _, err := client.Act(ctx, stagehand.ActInstruction("open the billing page"), nil); err != nil {
return err
}
// Extract returns data decoded into a Go type
extracted, err := stagehand.Extract[invoices](
ctx,
client,
"extract every invoice in the table",
nil,
)
if err != nil {
return err
}
fmt.Println(extracted.Data.Invoices)
return nil
}
</details>
Install
pnpm add @browserbasehq/stagehand 'zod@~4.4.3'
<details>
<summary><b>Python</b></summary>
pip install stagehand
</details>
<details>
<summary><b>Go</b></summary>
go get github.com/browserbase/stagehand/packages/sdk-go/v4@v4.0.0
</details>
Local runs need Chrome installed. Full setup: Quickstart.
<details> <summary><b>Using a coding agent? (Claude Code, Cursor, Codex)</b></summary>Install the browse CLI and skill instead of using the SDK directly:
npm install -g browse
npx skills add browserbase/stagehand --skill browse
browse open https://example.com --local
browse snapshot
browse get title
No LLM key or Browserbase account needed for local use. See CLI vs SDK vs hosted MCP for when to use each.
</details>Why Stagehand
<table> <tr> <td><strong>Familiar APIs</strong></td> <td>The Playwright-style methods you and your agents already know: <code>goto</code>, <code>click</code>, <code>locator</code>, <code>screenshot</code>.</td> </tr> <tr> <td><strong>Token efficiency</strong></td> <td>Hybrid accessibility-tree trimming gives agents exactly the page context they need and nothing more.</td> </tr> <tr> <td><strong>Faster in production</strong></td> <td>Stagehand runs as an extension next to the browser, cutting round-trip latency on every action.</td> </tr> <tr> <td><strong>Self-healing</strong></td> <td><code>act</code>, <code>observe</code>, and <code>extract</code> refresh how an action happens when the site changes underneath it.</td> </tr> <tr> <td><strong>Built for agents</strong></td> <td>WebMCP, clipboard support, batch commands, deep locators for nested iframes and closed Shadow DOMs, OTel traces.</td> </tr> <tr> <td><strong>Three languages</strong></td> <td>One complete browser driver across TypeScript, Python, and Go.</td> </tr> </table>Run it on Browserbase
Point the same script at Browserbase and get 2x faster execution than Playwright cloud equivalent browsers. Configure the Model Gateway so you never wire up a provider, and enable server-side caching to cache repeated actions.
import { browserbase, Stagehand } from "@browserbasehq/stagehand";
const browser = await browserbase.launch({ apiKey: process.env.BROWSERBASE_API_KEY! });
// No model configuration: the Model Gateway picks the cheapest model for each action
// cache: true: identical calls come back from Browserbase, no tokens spent
const stagehand = await Stagehand.create({ browser, cache: true });
<details>
<summary><b>Python</b></summary>
import os
from stagehand import Stagehand, browserbase
browser = await browserbase.launch(api_key=os.environ["BROWSERBASE_API_KEY"])
# No model configuration: the Model Gateway picks the cheapest model for each action
# cache=True: identical calls come back from Browserbase, no tokens spent
stagehand = await Stagehand.create(browser=browser, cache=True)
</details>
<details>
<summary><b>Go</b></summary>
browser, err := stagehand.LaunchBrowserbase(ctx, stagehand.BrowserbaseLaunchOptions{
APIKey: os.Getenv("BROWSERBASE_API_KEY"),
})
if err != nil {
return err
}
// No model configuration: the Model Gateway picks the cheapest model for each action
// CacheEnabled(true): identical calls come back from Browserbase, no tokens spent
cache := stagehand.CacheEnabled(true)
client, err := stagehand.Create(ctx, stagehand.CreateOptions{
Browser: browser,
Cache: &cache,
})
if err != nil {
return err
}
</details>
Verified mode, residential proxies, persistent contexts, and session recordings come with it. Get an API key and learn how to configure your browser here.
Hosted MCP: browser tools without installing anything
The hosted Browserbase MCP server puts navigate, act, observe, and extract in any MCP client — no install, no local browser. Requires a Browserbase API key.
claude mcp add --transport http browserbase https://mcp.browserbase.com/mcp \
--header "Authorization: Bearer $BROWSERBASE_API_KEY"
<details>
<summary><b>Cursor, Codex, and other MCP clients</b></summary>
{
"mcpServers": {
"browserbase": {
"url": "https://mcp.browserbase.com/mcp",
"headers": { "Authorization": "Bearer YOUR_BROWSERBASE_API_KEY" }
}
}
}
</details>
For local browsing without an API key, use the browse CLI instead.
Search and fetch without a browser
Fetch lets you grab the content of any URL as markdown. Search provides fast, token-efficient web search results. Both as a lightweight complement to browser sessions.
import { browserbase } from "@browserbasehq/stagehand";
const { results } = await browserbase.search({
apiKey: process.env.BROWSERBASE_API_KEY!,
query: "browser agent frameworks",
numResults: 5,
});
const fetched = await browserbase.fetch({
apiKey: process.env.BROWSERBASE_API_KEY!,
url: results[0].url,
format: "markdown",
});
console.log(fetched.content);
Docs and resources
<table> <tr> <td><a href="https://docs.stagehand.dev/v4/first-steps/quickstart">Quickstart</a></td> <td>Empty directory to working automation in three steps</td> </tr> <tr> <td><a href="https://docs.stagehand.dev/v4/reference/page">page</a> · <a href="https://docs.stagehand.dev/v4/reference/locator">locator</a></td> <td>Playwright-style browser and element APIs</td> </tr> <tr> <td><a href="https://docs.stagehand.dev/v4/basics/webmcp">WebMCP</a></td> <td>Discover and invoke WebMCP tools exposed by web pages</td> </tr> <tr> <td><a href="https://docs.stagehand.dev/v4/basics/act">act</a> · <a href="https://docs.stagehand.dev/v4/basics/extract">extract</a> · <a href="https://docs.stagehand.dev/v4/basics/observe">observe</a></td> <td>Browser actions and data extraction with natural language</td> </tr> <tr> <td><a href="https://docs.stagehand.dev/v4/add-ons/search">Search</a> · <a href="https://docs.stagehand.dev/v4/add-ons/fetch">Fetch</a></td> <td>Web search and page content without a browser</td> </tr> <tr> <td><a href="https://docs.stagehand.dev/v4/migrations/playwright">Migrate from Playwright</a></td> <td>Port an existing suite</td> </tr> <tr> <td><a href="https://docs.stagehand.dev/v4/integrations/overview">Integrations</a></td> <td>Mastra, Deep Agents, Vercel AI SDK, Claude Code, Codex</td> </tr> <tr> <td><a href="./packages/sdk-python/README.md">Python SDK</a> · <a href="https://github.com/browserbase/stagehand/blob/v4-go/packages/sdk-go/README.md">Go SDK</a></td> <td>Language-specific guides</td> </tr> <tr> <td><a href="https://deepwiki.com/browserbase/stagehand">Ask DeepWiki</a></td> <td>Ask questions about this codebase</td> </tr> </table>Join the community
Stagehand is built in the open, and the fastest way to shape it is to show up.
- ⭐ Star this repo — it is how most people find Stagehand
- 💬 Join the Discord — questions, support, and what we are building next
- 🐛 Open an issue — bug reports are the most useful contribution
- 𝕏 Follow @stagehanddev — releases and demos
Contributing
We're focused on improving reliability, extensibility, speed, and cost, in that order. Bug fixes and small improvements are the best way to get started. For anything larger, reach out to Miguel Gonzalez or Paul Klein on Discord first so we can make sure it lands.
Stagehand is a TypeScript, Python, and Go monorepo driven by just:
git clone https://github.com/browserbase/stagehand.git
cd stagehand
just install
just generate
just build
export OPENAI_API_KEY="your-openai-api-key"
just example act # runs packages/sdk-ts/examples/act.ts
See CONTRIBUTING.md for the full TypeScript, Python, and Go setup.
Acknowledgements
We'd like to thank the following people for their major contributions to Stagehand:
- Paul Klein
- Sean McGuire
- Miguel Gonzalez
- Sameel Arif
- Thomas Katwan
- Filip Michalsky
- Anirudh Kamath
- Jeremy Press
- Navid Pour
- Nick Sweeting
- Sam Finton
- Shrey Pandya
- Shriya Lolabattu
- Alyssa Maruyama
- Amel Bajramovic
License
Licensed under the MIT License.
Copyright 2026 Browserbase, Inc.
"Stagehand" is a trademark of Browserbase, Inc.
Stagehand alternatives
View more →Similar ai agents in Browser Automation, ranked by GitHub stars. Compare licence, recent activity and scan status before you choose.
| Name | Stars | Licence | Last updated | Scan |
|---|---|---|---|---|
| Stagehand (this page) browserbase | 26k | MIT | Oct 30, 2026 | Scanned, no flagged issues |
| Browser Use browser-use | 120k | MIT | Oct 31, 2026 | Scanned, no flagged issues |
| CloakBrowser CloakHQ | 33k | MIT | Oct 31, 2026 | Scanned, no flagged issues |
| Invisible Dots feder-cr | 32k | MIT | Oct 31, 2026 | Scanned, no flagged issues |
| OpenCLI jackwener | 31k | Apache-2.0 | Oct 26, 2026 | Scanned, no flagged issues |
| Moli lexmount | 22k | Apache-2.0 | Nov 1, 2026 | Scanned, no flagged issues |