โ† Back to Coding Agents & IDEs

browse

Complete guide for creating and deploying browser automation functions

0
Source Code

Browser Automation & Functions Skill

Complete guide for creating and deploying browser automation functions using the stagehand CLI.

When to Use

  • User wants to automate a website task
  • User needs to scrape data from a site
  • User wants to create a Browserbase Function
  • User wants to deploy automation to run on a schedule or via webhook

Prerequisites

Set Up Credentials

stagehand fn auth status  # Check if configured
stagehand fn auth login   # If needed - get credentials from https://browserbase.com/settings

Complete Workflow

Step 1: Explore the Site Interactively

Start a local browser session to understand the site structure:

stagehand session create --local
stagehand goto https://example.com
stagehand snapshot                    # Get DOM structure with refs
stagehand screenshot -o page.png      # Visual inspection

Test interactions manually:

stagehand click @0-5
stagehand fill @0-6 "value"
stagehand eval "document.querySelector('.price').textContent"
stagehand session end  # When done exploring

Step 2: Initialize Function Project

stagehand fn init my-automation
cd my-automation

Creates:

  • package.json - Dependencies
  • .env - Credentials (from ~/.stagehand/config.json)
  • index.ts - Function template
  • tsconfig.json - TypeScript config

Step 3: โš ๏ธ FIX package.json IMMEDIATELY

CRITICAL BUG: stagehand fn init generates incomplete package.json that causes deployment to fail with "No functions were built."

REQUIRED FIX - Update package.json before doing anything else:

{
  "name": "my-automation",
  "version": "1.0.0",
  "description": "My automation description",
  "main": "index.js",
  "type": "module",
  "packageManager": "[email protected]",
  "scripts": {
    "dev": "pnpm bb dev index.ts",
    "publish": "pnpm bb publish index.ts"
  },
  "dependencies": {
    "@browserbasehq/sdk-functions": "^0.0.5",
    "playwright-core": "^1.58.0"
  },
  "devDependencies": {
    "@types/node": "^25.0.10",
    "typescript": "^5.9.3"
  }
}

Key changes from generated file:

  • โœ… Add description and main fields
  • โœ… Add packageManager field
  • โœ… Change "latest" to pinned versions like "^0.0.5"
  • โœ… Add devDependencies with TypeScript and types

Then install:

pnpm install

Step 4: Write Automation Code

Edit index.ts:

import { defineFn } from "@browserbasehq/sdk-functions";
import { chromium } from "playwright-core";

defineFn("my-automation", async (context) => {
  const { session, params } = context;
  console.log("Connecting to browser session:", session.id);

  const browser = await chromium.connectOverCDP(session.connectUrl);
  const page = browser.contexts()[0]!.pages()[0]!;

  // Your automation here
  await page.goto("https://example.com");
  await page.waitForLoadState("domcontentloaded");

  // Extract data
  const data = await page.evaluate(() => {
    // Complex extraction logic
    return Array.from(document.querySelectorAll('.item')).map(el => ({
      title: el.querySelector('.title')?.textContent,
      value: el.querySelector('.value')?.textContent,
    }));
  });

  // Return results (must be JSON-serializable)
  return {
    success: true,
    count: data.length,
    data,
    timestamp: new Date().toISOString(),
  };
});

Key Concepts:

  • context.session - Browser session info (id, connectUrl)
  • context.params - Input parameters from invocation
  • Return JSON-serializable data
  • 15 minute max execution time

Step 5: Test Locally

Start dev server:

pnpm bb dev index.ts

Server runs at http://127.0.0.1:14113

Invoke with curl:

curl -X POST http://127.0.0.1:14113/v1/functions/my-automation/invoke \
  -H "Content-Type: application/json" \
  -d '{"params": {"url": "https://example.com"}}'

Dev server auto-reloads on file changes. Check terminal for logs.

Step 6: Deploy to Browserbase

pnpm bb publish index.ts
# or: stagehand fn publish index.ts

Expected output:

โœ“ Build completed successfully
Build ID: xxx-xxx-xxx
Function ID: yyy-yyy-yyy  โ† Save this!

If you see "No functions were built" โ†’ Your package.json is incomplete (see Step 3).

Step 7: Test Production

stagehand fn invoke <function-id> -p '{"param": "value"}'

Or via API:

curl -X POST https://api.browserbase.com/v1/functions/<function-id>/invoke \
  -H "Content-Type: application/json" \
  -H "x-bb-api-key: $BROWSERBASE_API_KEY" \
  -d '{"params": {}}'

Complete Working Example: Hacker News Scraper

import { defineFn } from "@browserbasehq/sdk-functions";
import { chromium } from "playwright-core";

defineFn("hn-scraper", async (context) => {
  const { session } = context;
  console.log("Connecting to browser session:", session.id);

  const browser = await chromium.connectOverCDP(session.connectUrl);
  const page = browser.contexts()[0]!.pages()[0]!;

  await page.goto("https://news.ycombinator.com");
  await page.waitForLoadState("domcontentloaded");

  // Extract top 10 stories
  const stories = await page.evaluate(() => {
    const storyRows = Array.from(document.querySelectorAll('.athing')).slice(0, 10);

    return storyRows.map((row) => {
      const titleLine = row.querySelector('.titleline a');
      const subtext = row.nextElementSibling?.querySelector('.subtext');
      const commentsLink = Array.from(subtext?.querySelectorAll('a') || []).pop();

      return {
        rank: row.querySelector('.rank')?.textContent?.replace('.', '') || '',
        title: titleLine?.textContent || '',
        url: titleLine?.getAttribute('href') || '',
        points: subtext?.querySelector('.score')?.textContent?.replace(' points', '') || '0',
        author: subtext?.querySelector('.hnuser')?.textContent || '',
        time: subtext?.querySelector('.age')?.textContent || '',
        comments: commentsLink?.textContent?.replace(/\u00a0comments?/, '').trim() || '0',
        id: row.id,
      };
    });
  });

  return {
    success: true,
    count: stories.length,
    stories,
    timestamp: new Date().toISOString(),
  };
});

Common Patterns

Parameterized Scraping

defineFn("scrape", async (context) => {
  const { session, params } = context;
  const { url, selector } = params;  // Accept params from invocation

  const browser = await chromium.connectOverCDP(session.connectUrl);
  const page = browser.contexts()[0]!.pages()[0]!;

  await page.goto(url);
  const data = await page.$$eval(selector, els =>
    els.map(el => el.textContent)
  );

  return { url, data };
});

Authentication

defineFn("auth-action", async (context) => {
  const { session, params } = context;
  const { username, password } = params;

  const browser = await chromium.connectOverCDP(session.connectUrl);
  const page = browser.contexts()[0]!.pages()[0]!;

  await page.goto("https://example.com/login");
  await page.fill('input[name="email"]', username);
  await page.fill('input[name="password"]', password);
  await page.click('button[type="submit"]');
  await page.waitForURL("**/dashboard");

  const data = await page.textContent('.user-data');
  return { success: true, data };
});

Multi-Page Workflow

defineFn("multi-page", async (context) => {
  const { session, params } = context;
  const browser = await chromium.connectOverCDP(session.connectUrl);
  const page = browser.contexts()[0]!.pages()[0]!;

  const results = [];
  for (const url of params.urls) {
    await page.goto(url);
    await page.waitForLoadState("domcontentloaded");

    const title = await page.title();
    results.push({ url, title });
  }

  return { results };
});

Troubleshooting

๐Ÿ”ด "No functions were built. Please check your entrypoint and function exports."

This is the #1 error!

Cause: Generated package.json from stagehand fn init is incomplete.

Fix:

  1. Update package.json (see Step 3 above)
  2. Add all required fields: description, main, packageManager
  3. Change "latest" to pinned versions like "^0.0.5"
  4. Add devDependencies section with TypeScript and types
  5. Run pnpm install
  6. Try deploying again

Quick check: Compare your package.json to bitcoin-functions/package.json in the codebase.

Local dev server won't start

# Check credentials
stagehand fn auth status

# Re-login if needed
stagehand fn auth login

# Install SDK globally
pnpm add -g @browserbasehq/sdk-functions

Function works locally but fails on deploy

Common causes:

  1. Missing devDependencies (TypeScript won't compile)
  2. Using "latest" instead of pinned versions
  3. Missing required fields in package.json

Solution: Fix package.json as described in Step 3.

Cannot extract data from page

  1. Take screenshot: stagehand screenshot -o debug.png
  2. Get snapshot: stagehand snapshot
  3. Use page.evaluate() to log what's in the DOM
  4. Check if selectors match actual HTML structure

"Invocation timed out"

  • Functions have 15 minute max
  • Use specific waits instead of long sleeps
  • Check if page is actually loading

Best Practices

  1. โœ… Fix package.json immediately after stagehand fn init
  2. โœ… Explore interactively first - Use local browser session to understand site
  3. โœ… Test manually - Verify each step works before writing code
  4. โœ… Test locally - Use dev server before deploying
  5. โœ… Return meaningful data - Include timestamps, counts, URLs
  6. โœ… Handle errors gracefully - Try/catch around risky operations
  7. โœ… Use specific selectors - Prefer data attributes over CSS classes
  8. โœ… Add logging - console.log() helps debug deployed functions
  9. โœ… Validate parameters - Check params before using
  10. โœ… Set reasonable timeouts - Don't wait forever

Quick Checklist

  • Explore site with stagehand session create --local
  • Test interactions manually
  • Create project: stagehand fn init <name>
  • Fix package.json immediately (Step 3)
  • Run pnpm install
  • Write automation in index.ts
  • Test locally: pnpm bb dev index.ts
  • Verify with curl
  • Deploy: pnpm bb publish index.ts
  • Test production: stagehand fn invoke <function-id>
  • Save function ID

Code Fix Needed (For Maintainers)

File: /src/commands/functions.ts Lines: 146-158 Function: initFunction()

Replace the current packageJson object with:

const packageJson = {
  name,
  version: '1.0.0',
  description: `${name} function`,
  main: 'index.js',
  type: 'module',
  packageManager: '[email protected]',
  scripts: {
    dev: 'pnpm bb dev index.ts',
    publish: 'pnpm bb publish index.ts',
  },
  dependencies: {
    '@browserbasehq/sdk-functions': '^0.0.5',
    'playwright-core': '^1.58.0',
  },
  devDependencies: {
    '@types/node': '^25.0.10',
    'typescript': '^5.9.3',
  },
};

This will eliminate the "No functions were built" error for all new projects.