Playwright selectors break every time the frontend team ships a redesign. The button you were clicking with button[data-testid="submit-payment"] got renamed. The element hierarchy changed. Your test suite is red and the automation is down.
Stagehand fixes this by replacing selectors with natural language. Instead of:
await page.click('button[data-testid="submit-payment"]')
You write:
await stagehand.act("click the Submit Payment button")
The model figures out which element to click. When the frontend changes, the instruction still works. You're describing intent, not implementation.
What Stagehand is
Stagehand is an open-source framework by Browserbase that wraps Playwright with AI. Three core methods:
act(instruction)— take an action on the page ("click X", "type Y into Z", "scroll down")observe(instruction)— find elements and describe what's there ("find all product prices", "what navigation tabs are visible?")extract(options)— pull structured data from the page using a Zod schema
Setup
npm install @browserbasehq/stagehand zod
You need an Anthropic or OpenAI API key. Stagehand uses vision models to understand the page — each act() makes one model call.
import { Stagehand } from "@browserbasehq/stagehand";
import { z } from "zod";
const stagehand = new Stagehand({
env: "LOCAL", // or "BROWSERBASE" for cloud
modelName: "claude-sonnet-4-6",
modelClientOptions: {
apiKey: process.env.ANTHROPIC_API_KEY,
},
headless: false, // true for production
verbose: 1,
});
await stagehand.init();
const page = stagehand.page;
Core methods in action
act() — take actions
await page.goto("https://example-shop.com");
// Natural language actions
await stagehand.act("search for 'wireless headphones'");
await stagehand.act("click on the first product in the results");
await stagehand.act("add the item to the cart");
await stagehand.act("proceed to checkout");
// Forms
await stagehand.act("fill in the email field with test@example.com");
await stagehand.act("select 'Standard Shipping' from the shipping options");
Each act() is one model call. Stagehand takes a screenshot, sends it to the vision model, identifies the element, and uses Playwright to interact with it.
observe() — understand the page
const observations = await stagehand.observe(
"find all product cards on this page with their prices and availability status"
);
console.log(observations);
// [
// { element: "product-card-1", description: "Sony WH-1000XM5, ₹24,990, In stock" },
// { element: "product-card-2", description: "Bose QuietComfort 45, ₹27,999, Out of stock" },
// ...
// ]
observe() is non-destructive — it looks but doesn't interact. Use it to understand page state before acting.
extract() — pull structured data
import { z } from "zod";
const ProductSchema = z.object({
name: z.string(),
price: z.number(),
original_price: z.number().optional(),
discount_percent: z.number().optional(),
availability: z.enum(["in_stock", "out_of_stock", "limited"]),
rating: z.number().min(0).max(5).optional(),
review_count: z.number().optional(),
});
const ProductListSchema = z.object({
products: z.array(ProductSchema),
total_count: z.number().optional(),
});
const data = await stagehand.extract({
instruction: "Extract all product listings with their prices and availability",
schema: ProductListSchema,
});
console.log(data.products);
// [
// { name: "Sony WH-1000XM5", price: 24990, availability: "in_stock", rating: 4.4, review_count: 2847 },
// ...
// ]
The Zod schema ensures type safety — extract() validates the output against the schema and returns a fully typed object.
A complete competitor price monitor
This agent visits competitor product pages daily, extracts pricing data, and compares against your prices:
import { Stagehand } from "@browserbasehq/stagehand";
import { z } from "zod";
const PriceSchema = z.object({
product_name: z.string(),
current_price: z.number(),
original_price: z.number().optional(),
in_stock: z.boolean(),
last_updated: z.string().optional(),
});
const TARGET_PRODUCTS = [
{
slug: "wireless-headphones-xm5",
your_price: 24990,
competitor_urls: [
"https://www.amazon.in/dp/B0XXXXXXXX",
"https://www.flipkart.com/sony-wh1000xm5",
],
},
// ... more products
];
async function scrapePrice(stagehand: Stagehand, url: string): Promise<PriceSchema | null> {
try {
await stagehand.page.goto(url, { waitUntil: "domcontentloaded", timeout: 15000 });
// Wait for price to be visible
await stagehand.observe("find the product price");
const data = await stagehand.extract({
instruction: "Extract the current selling price, original price if discounted, and whether the item is in stock",
schema: PriceSchema,
});
return data;
} catch (e) {
console.error(`Failed to scrape ${url}: ${e}`);
return null;
}
}
async function runPriceMonitor() {
const stagehand = new Stagehand({
env: "LOCAL",
modelName: "claude-haiku-4-5-20251001", // Haiku for cost
modelClientOptions: { apiKey: process.env.ANTHROPIC_API_KEY },
headless: true,
});
await stagehand.init();
const results = [];
for (const product of TARGET_PRODUCTS) {
for (const url of product.competitor_urls) {
console.log(`Scraping: ${url}`);
const price_data = await scrapePrice(stagehand, url);
if (price_data?.current_price) {
const diff_pct = ((product.your_price - price_data.current_price) / price_data.current_price) * 100;
results.push({
slug: product.slug,
your_price: product.your_price,
competitor_price: price_data.current_price,
competitor_url: url,
diff_pct: Math.round(diff_pct * 10) / 10,
status: diff_pct > 15 ? "overpriced" : diff_pct < -15 ? "underpriced" : "competitive",
in_stock: price_data.in_stock,
});
}
// Polite delay between requests
await new Promise(r => setTimeout(r, 2000));
}
}
await stagehand.close();
// Print report
const overpriced = results.filter(r => r.status === "overpriced");
console.log(`\nPrice Monitor Report — ${new Date().toLocaleDateString()}`);
console.log(`${results.length} comparisons done`);
console.log(`Overpriced: ${overpriced.length}`);
overpriced.forEach(r =>
console.log(` ${r.slug}: ₹${r.your_price} vs ₹${r.competitor_price} (+${r.diff_pct}%)`)
);
return results;
}
runPriceMonitor();
Handling dynamic content
Some pages load prices asynchronously with JavaScript. Two approaches:
// Wait for a specific element before extracting
await page.waitForSelector("[data-price]", { timeout: 10000 });
const data = await stagehand.extract({ ... });
// Or use observe() to wait for visible content
await stagehand.observe("wait for the product price to load and be visible");
const data = await stagehand.extract({ ... });
For Cloudflare-protected pages or sites that detect headless browsers, Stagehand has a Browserbase cloud mode that uses real browser fingerprints:
const stagehand = new Stagehand({
env: "BROWSERBASE",
apiKey: process.env.BROWSERBASE_API_KEY,
projectId: process.env.BROWSERBASE_PROJECT_ID,
// ... same API
});
When act() fails
Stagehand falls back to standard Playwright when the AI fails to identify an element. You can catch failures and add fallbacks:
try {
await stagehand.act("click the Add to Cart button");
} catch (e) {
// AI couldn't find it — try Playwright directly
await page.click('button[data-action="add-to-cart"]');
}
Debug mode shows you what the model sees and what element it chose:
const stagehand = new Stagehand({
verbose: 2, // Full debug logging
// ...
});
Cost per workflow
Each act(), observe(), and extract() call = one vision model call. A 10-step workflow with Claude Haiku costs ~$0.02. The same workflow with Sonnet: ~$0.15.
For production scraping at scale, use Haiku. For complex multi-step workflows with dynamic pages, Sonnet is more reliable.
Stagehand vs alternatives
| Tool | Language | Reliability | Cost/step | Best for |
|---|---|---|---|---|
| Stagehand | JS/TS | High | Low (Haiku) | Production web automation |
| browser-use | Python | Medium | Medium | Quick Python prototypes |
| Computer use API | Python | Lower | High | Legacy desktop apps |
| Playwright alone | Any | High | Zero | Stable, tested selectors |
Use plain Playwright when: the site is stable and you can maintain selectors. Use Stagehand when: the site changes frequently, you can't inspect DOM, or you need structured data extraction from arbitrary pages.
The Firecrawl post covers server-side scraping for cases where you need to avoid browser overhead. Stagehand is the right choice when you need actual browser interaction — forms, clicks, multi-step flows.



