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 result

observe(): 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