What Stagehand Does Differently
Traditional browser automation breaks when the page changes. A button moves, a class name updates, and your CSS selector stops working. Stagehand uses a vision model to interpret the page and translate natural language instructions into browser actions — making automations resilient to UI changes.
The tradeoff: LLM calls on every action add latency (typically 1-3 seconds per act() or extract() call). For interactive workflows that run once per user request this is fine. For high-volume batch scraping, raw Playwright is faster.
The Three Core Methods
| Method | What it does | Returns |
|---|---|---|
| session.act(input=...) | Performs an action on the page (click, fill, navigate) | None — side effect only |
| session.extract(instruction=..., schema=...) | Extracts structured data from the current page | Dict matching your schema |
| session.observe(instruction=...) | Returns a list of actionable elements without acting | List of element descriptors |
act(): Performing Page Actions
from stagehand import AsyncStagehand
import asyncio
async def main():
client = AsyncStagehand()
session = await client.sessions.create(model_name='openai/gpt-5-nano')
await session.navigate(url='https://news.ycombinator.com')
# Click actions
await session.act(input='Click on the first story link')
# Form filling
await session.navigate(url='https://github.com/login')
await session.act(input='Fill in the username field with myuser@example.com')
await session.act(input='Fill in the password field with my_password')
await session.act(input='Click the Sign in button')
await session.end()
asyncio.run(main())Write act() instructions as specific commands, not descriptions. 'Click the blue Submit button in the checkout form' is better than 'Submit the form' — the model has more context to locate the correct element.extract(): Getting Structured Data
extract() returns data matching a schema you describe. Use Zod schemas in TypeScript, or a JSON Schema dict in Python, for structured output.
import asyncio
from stagehand import AsyncStagehand
# v3 Python uses a JSON Schema dict, not a Pydantic model
PRODUCT_LIST_SCHEMA = {
'type': 'object',
'properties': {
'products': {
'type': 'array',
'items': {
'type': 'object',
'properties': {
'name': {'type': 'string'},
'price': {'type': 'string'},
'rating': {'type': 'string'},
'in_stock': {'type': 'boolean'},
},
'required': ['name', 'price'],
},
},
},
'required': ['products'],
}
async def scrape_products(url: str):
client = AsyncStagehand()
session = await client.sessions.create(model_name='openai/gpt-5-nano')
await session.navigate(url=url)
# Extract with a JSON Schema
result = await session.extract(
instruction='Extract all product listings visible on this page',
schema=PRODUCT_LIST_SCHEMA,
)
print(result)
await session.end()
return resultobserve(): Planning Before Acting
observe() is useful when you want to check what is available on a page before deciding what to do. It returns actionable elements without executing any action — useful for building agents that reason about their options.
async def smart_navigation(session, goal: str):
# First observe what options are available
observed = await session.observe(
instruction=f'What navigation links or buttons are available to help accomplish: {goal}'
)
print('Available actions:')
for option in observed.results:
print(f' - {option.description}')
# Then decide and act based on observations
if observed.results:
action = observed.results[0].to_dict(exclude_none=True)
await session.act(input=action)Session Management and Resumption
For long-running tasks, save your session ID and resume it if interrupted. This preserves cookies, local storage, and authentication state.
import os
import json
from stagehand import AsyncStagehand
async def run_with_session_persistence(task_id: str):
client = AsyncStagehand()
# Try to resume an existing Browserbase session by id
session_file = f'sessions/{task_id}.json'
if os.path.exists(session_file):
with open(session_file) as f:
saved = json.load(f)
session = await client.sessions.create(
model_name='openai/gpt-5-nano',
browserbase_session_id=saved['session_id'],
)
else:
session = await client.sessions.create(model_name='openai/gpt-5-nano')
# Save the session id for potential resumption
os.makedirs('sessions', exist_ok=True)
with open(session_file, 'w') as f:
json.dump({'session_id': session.id}, f)
# ... perform task ...
await session.end()Sessions created in Browserbase have a maximum duration (typically 15 minutes for free tier). For longer tasks, break them into multiple sessions and use a database to store intermediate state rather than relying on browser session persistence.Common Failure Patterns
| Problem | Cause | Fix |
|---|---|---|
| act() does nothing | Instruction too vague for model to locate element | Be more specific: include element type, location, or unique text |
| extract() returns empty or wrong data | Page uses heavy JS rendering, content not yet visible | Add await page.wait_for_load_state('networkidle') before extract() |
| High latency (5+ seconds per action) | LLM call overhead on every act() | Batch related actions; use raw Playwright for known-stable selectors |
| Authentication breaks after session resume | Session expired or cookies invalidated | Re-authenticate at start of each session rather than relying on resumption |