Stagehand is the SDK to extract data and interact with any site on the web.

Playwright was built for testing. Stagehand is built for agents, in TypeScript, Python, and Go.

Docs · Quickstart · ⭐ Star this repo

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();Python

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())Go

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

}pnpm add @browserbasehq/stagehand 'zod@~4.4.3'Python

pip install stagehandGo

go get github.com/browserbase/stagehand/packages/sdk-go/v4@v4.0.0Local runs need Chrome installed. Full setup: Quickstart.

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 });Python

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)Go

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

}Verified mode, residential proxies, persistent contexts, and session recordings come with it. Get an API key and learn how to configure your browser here.

The hosted Browserbase MCP server puts navigate, act, observe, and extract in any MCP client — no install, no local browser.

claude mcp add --transport http browserbase https://mcp.browserbase.com/mcp \

--header "Authorization: Bearer $BROWSERBASE_API_KEY"Cursor, Codex, and other MCP clients

{

"mcpServers": {

"browserbase": {

"url": "https://mcp.browserbase.com/mcp",

"headers": { "Authorization": "Bearer YOUR_BROWSERBASE_API_KEY" }

}

}

}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);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

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.tsSee CONTRIBUTING.md for the full TypeScript, Python, and Go setup.

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

Licensed under the MIT License.

Copyright 2026 Browserbase, Inc.

"Stagehand" is a trademark of Browserbase, Inc.