browseComplete guide for creating and deploying browser automation functions using the stagehand CLI
Complete guide for creating and deploying browser automation functions using the stagehand CLI.
stagehand fn auth status # Check if configured
stagehand fn auth login # If needed - get credentials from https://browserbase.com/settings
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
stagehand fn init my-automation
cd my-automation
Creates:
package.json - Dependencies.env - Credentials (from ~/.stagehand/config.json)index.ts - Function templatetsconfig.json - TypeScript configCRITICAL 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": "pnpm@10.14.0",
"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:
description and main fieldspackageManager field"latest" to pinned versions like "^0.0.5"devDependencies with TypeScript and typesThen install:
pnpm install
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 invocationStart 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.
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).
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": {}}'
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(),
};
});
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 };
});
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 };
});
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 };
});
This is the #1 error!
Cause: Generated package.json from stagehand fn init is incomplete.
Fix:
package.json (see Step 3 above)description, main, packageManager"latest" to pinned versions like "^0.0.5"devDependencies section with TypeScript and typespnpm installQuick check: Compare your package.json to bitcoin-functions/package.json in the codebase.
# Check credentials
stagehand fn auth status
# Re-login if needed
stagehand fn auth login
# Install SDK globally
pnpm add -g @browserbasehq/sdk-functions
Common causes:
devDependencies (TypeScript won't compile)"latest" instead of pinned versionspackage.jsonSolution: Fix package.json as described in Step 3.
stagehand screenshot -o debug.pngstagehand snapshotpage.evaluate() to log what's in the DOMstagehand fn initconsole.log() helps debug deployed functionsparams before usingstagehand session create --localstagehand fn init pnpm installindex.tspnpm bb dev index.tspnpm bb publish index.tsstagehand fn invoke 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: 'pnpm@10.14.0',
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.
Generated Mar 1, 2026
Automate daily checks on competitor product pages to scrape pricing, availability, and promotional data. This enables retailers to adjust their own pricing strategies dynamically and maintain competitive advantage in fast-moving markets.
Scrape property details, images, and contact information from multiple real estate websites to create a centralized database. This helps agencies and investors quickly analyze market trends and identify new opportunities without manual data entry.
Automate the collection of news articles and social media posts from financial websites to extract sentiment and key metrics. This supports traders and analysts in making data-driven decisions by monitoring market reactions and trends in real-time.
Scrape job listings from various career sites to gather information on roles, salaries, and required skills. This assists HR departments and recruitment agencies in understanding labor market demands and optimizing hiring strategies efficiently.
Automate the extraction of scholarly articles, citations, and datasets from academic journals and repositories. This enables researchers to compile comprehensive literature reviews and analyze trends without spending hours on manual searches.
Offer a SaaS platform where businesses subscribe to receive automated, real-time data feeds from web sources. Revenue is generated through monthly or annual fees, with tiered pricing based on data volume and frequency of updates.
Provide bespoke browser automation solutions tailored to client-specific needs, such as scraping proprietary websites or integrating with internal systems. Revenue comes from one-time project fees or ongoing maintenance contracts.
Deploy functions as APIs that clients can call to retrieve processed data on-demand, charging per API call or based on usage tiers. This model scales easily with client demand and reduces infrastructure overhead for users.
š¬ Integration Tip
Ensure the package.json is correctly configured with pinned dependencies and required fields to avoid deployment failures, and use the local dev server for thorough testing before publishing.
A fast Rust-based headless browser automation CLI with Node.js fallback that enables AI agents to navigate, click, type, and snapshot pages via structured commands.
Automate web browser interactions using natural language via CLI commands. Use when the user asks to browse websites, navigate web pages, extract data from websites, take screenshots, fill forms, click buttons, or interact with web applications.
Advanced desktop automation with mouse, keyboard, and screen control
Manage n8n workflows and automations via API. Use when working with n8n workflows, executions, or automation tasks - listing workflows, activating/deactivating, checking execution status, manually triggering workflows, or debugging automation issues.
Design and implement automation workflows to save time and scale operations as a solopreneur. Use when identifying repetitive tasks to automate, building workflows across tools, setting up triggers and actions, or optimizing existing automations. Covers automation opportunity identification, workflow design, tool selection (Zapier, Make, n8n), testing, and maintenance. Trigger on "automate", "automation", "workflow automation", "save time", "reduce manual work", "automate my business", "no-code automation".
Browser automation via Playwright MCP server. Navigate websites, click elements, fill forms, extract data, take screenshots, and perform full browser automation workflows.