Skip to main content

Overview

Stagehand is an AI-powered browser automation framework with methods like observe(), extract(), and act(). This guide walks you through deploying and running a Stagehand project on Intuned. You’ll build a sample scraper that extracts book data—without writing Playwright selectors or custom parsing logic. The same patterns apply to any automation you build with the framework.
This guide assumes you have a basic understanding of Intuned projects. If you’re new to Intuned, start with the getting started guide.

When to use AI automation

Intuned supports AI-powered browser automation frameworks like Stagehand, Browser Use, and others. Use AI automation when:
  • Pages are dynamic — Elements change position, structure, or content unpredictably
  • You don’t know the exact page structure — Scraping sites you haven’t mapped in detail
  • You want natural language control — Describe what to do instead of writing precise selectors
  • Traditional Playwright code is too brittle for your case — AI agents adapt to minor UI changes automatically
Stagehand is one option for AI automation on Intuned. The setup patterns in this guide apply to other AI frameworks as well. For a deeper dive into choosing between deterministic, AI-driven, and hybrid approaches, check out Flexible Automations.

Guide

Let’s build a getBooks API with AI. The template handles all the Stagehand setup for you. You can develop with Stagehand in two ways:
  • Online IDE — Zero setup. Write, test, and deploy directly from your browser.
  • CLI — Use your favorite IDE with full version control.
1

Create a project

  1. Go to Intuned dashboard
  2. Select + New Project > Templates > Stagehand
  3. Select your language (Python or TypeScript)
  4. Name it and select Create Project
Create Stagehand project from template
2

Explore the project

The template includes a book scraper API that combines Playwright with AI agents.Project structure:
my-stagehand-project/
├── api/
│   └── get-books.ts             # Main automation API
├── hooks/
│   └── setupContext.ts          # CDP URL setup
├── package.json                 # Dependencies
└── intuned.json                 # Project configuration
The automation code:
Python
from typing import TypedDict
from playwright.async_api import Page
from pydantic import BaseModel
from intuned_runtime import attempt_store, get_ai_gateway_config
from stagehand import AsyncStagehand
from stagehand.types.model_config_param import ModelConfigParam
from stagehand.types.session_start_params import Browser, BrowserLaunchOptions


class Params(TypedDict):
    category: str | None


class BookDetails(BaseModel):
    title: str
    price: str
    rating: str | None = None
    availability: str | None = None


class BooksResponse(BaseModel):
    books: list[BookDetails]


MAX_PAGES = 10


async def automation(page: Page, params: Params, **_kwargs):
    base_url, api_key = get_ai_gateway_config()
    cdp_url = attempt_store.get("cdp_url")

    model_name = "openai/gpt-5-mini"
    model_config: ModelConfigParam = {
        "model_name": model_name,
        "api_key": api_key,
        "base_url": base_url,
        "provider": "openai",
    }

    # Initialize Stagehand with act/extract/observe capabilities
    client = AsyncStagehand(
        server="local",
        model_api_key=api_key,
        local_ready_timeout_s=30.0,
    )
    launch_options: BrowserLaunchOptions = {"headless": False}
    if cdp_url is not None:
        launch_options["cdp_url"] = str(cdp_url)
    browser: Browser = {"type": "local", "launch_options": launch_options}
    session = await client.sessions.start(
        model_name=model_name,
        browser=browser,
    )
    session_id = session.data.session_id

    await page.set_viewport_size({"width": 1280, "height": 800})

    category = params.get("category")
    all_books: list[BookDetails] = []

    try:
        await client.sessions.navigate(
            id=session_id,
            url="https://books.toscrape.com",
        )

        # Navigate to category if specified using observe and act
        if category:
            await client.sessions.observe(
                id=session_id,
                instruction=f'the "{category}" category link in the sidebar',
                options={"model": model_config},
            )
            await client.sessions.act(
                id=session_id,
                input=f'Click on the "{category}" category link in the sidebar',
                options={"model": model_config},
            )

        # Collect books from all pages
        for page_num in range(1, MAX_PAGES + 1):
            # Extract all book details from the current page
            result = await client.sessions.extract(
                id=session_id,
                instruction="Extract all books on this page including title, price, rating and availability for each book",
                options={"model": model_config},
                schema={
                    "type": "object",
                    "properties": {
                        "books": {
                            "type": "array",
                            "items": {
                                "type": "object",
                                "properties": {
                                    "title": {"type": "string"},
                                    "price": {"type": "string"},
                                    "rating": {"type": "string"},
                                    "availability": {"type": "string"},
                                },
                                "required": ["title", "price"],
                            },
                        }
                    },
                    "required": ["books"],
                },
            )

            books_data = BooksResponse.model_validate(result.data.result)
            all_books.extend(books_data.books)

            # Check if there's a next page and navigate to it
            if page_num < MAX_PAGES:
                try:
                    next_button = await client.sessions.observe(
                        id=session_id,
                        instruction='the "next" button or link to go to the next page',
                        options={"model": model_config},
                    )
                    if not next_button.data.result:
                        break
                    await client.sessions.act(
                        id=session_id,
                        input='Click the "next" button to go to the next page',
                        options={"model": model_config},
                    )
                except Exception:
                    break

    finally:
        await client.sessions.end(session_id)

    return BooksResponse(books=all_books)
What this does: Navigates to https://books.toscrape.com, uses Stagehand’s observe() and act() methods to navigate to a specific category, then uses extract() to collect book details across multiple pages. TypeScript uses Zod schemas; Python uses a JSON schema dict validated into Pydantic models.
3

Run your automation

Run the book scraper to test your setup.
  1. In the Online IDE, select the API from the dropdown
  2. Select Select Parameter and enter {"category": "Travel"}
  3. Select Start Run
Running Stagehand automation in IDE
4

Deploy and test

Deploy your automation to Intuned’s infrastructure.
  1. In the Online IDE, select Deploy in the top-right corner
  2. After deployment completes, go to the project’s Runs page
  3. Select Start Run, then choose your API
  4. Enter parameters: {"category": "Travel"} and select Start Run
Running Stagehand automation in Dashboard
  1. After the run completes, view the extracted results
View Stagehand run results

How it works

  • The setupContext hook stores the CDP URL in attemptStore for your API to access
Check out our setup hook recipe.
  • Your API initializes Stagehand with the CDP URL and AI gateway credentials from getAiGatewayConfig()
  • Stagehand provides three core AI-powered methods:
    • observe() - Find elements on the page using natural language
    • act() - Perform actions like clicking using natural language
    • extract() - Extract structured data from the page with type-safe schemas
  • Passes AI credentials via the model config (modelName, apiKey, baseURL)
  • Call methods directly on stagehand (e.g., stagehand.act(), stagehand.extract())
  • Use the Playwright page for navigation (e.g., page.goto())
  • Your API accepts parameters (like category) that can vary for each run
  • The automation handles pagination automatically and cleans up Stagehand when done

Stagehand Documentation

Official Stagehand documentation and API reference

Stagehand GitHub

View the source code and contribute to Stagehand

Intuned Cookbook

Browse examples and recipes for Intuned projects

attemptStore Reference

Learn more about sharing data between hooks and APIs