├── app
├── favicon.ico
├── globals.css
├── layout.tsx
├── api
│ └── stagehand
│ │ ├── run.ts
│ │ └── main.ts
└── page.tsx
├── public
├── thumbnail.png
├── vercel.svg
├── window.svg
├── file.svg
├── browserbase.svg
├── globe.svg
├── browserbase_grayscale.svg
├── next.svg
├── logo_dark.svg
└── logo_light.svg
├── .example.env
├── postcss.config.mjs
├── next.config.ts
├── eslint.config.mjs
├── tailwind.config.ts
├── tsconfig.json
├── .gitignore
├── package.json
├── components
└── stagehand
│ └── debuggerIframe.tsx
├── README.md
├── stagehand.config.ts
└── .cursorrules
/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/browserbase/stagehand-nextjs-quickstart/HEAD/app/favicon.ico
--------------------------------------------------------------------------------
/public/thumbnail.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/browserbase/stagehand-nextjs-quickstart/HEAD/public/thumbnail.png
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.example.env:
--------------------------------------------------------------------------------
1 | BROWSERBASE_API_KEY=YOUR_BROWSERBASE_API_KEY
2 | BROWSERBASE_PROJECT_ID=YOUR_BROWSERBASE_PROJECT_ID
3 | OPENAI_API_KEY=YOUR_OPENAI_API_KEY
4 |
--------------------------------------------------------------------------------
/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | tailwindcss: {},
5 | },
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/next.config.ts:
--------------------------------------------------------------------------------
1 | import type { NextConfig } from "next";
2 |
3 | const nextConfig: NextConfig = {
4 | serverExternalPackages: [
5 | "@browserbasehq/stagehand",
6 | "@browserbasehq/sdk",
7 | "thread-stream",
8 | "pino",
9 | "pino-pretty",
10 | ],
11 | };
12 |
13 | export default nextConfig;
14 |
--------------------------------------------------------------------------------
/public/window.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/file.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | :root {
6 | --background: #ffffff;
7 | --foreground: #171717;
8 | }
9 |
10 | @media (prefers-color-scheme: dark) {
11 | :root {
12 | --background: #0E0D0C;
13 | --foreground: #ededed;
14 | }
15 | }
16 |
17 | body {
18 | color: var(--foreground);
19 | background: var(--background);
20 | font-family: Arial, Helvetica, sans-serif;
21 | }
22 |
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import { dirname } from "path";
2 | import { fileURLToPath } from "url";
3 | import { FlatCompat } from "@eslint/eslintrc";
4 |
5 | const __filename = fileURLToPath(import.meta.url);
6 | const __dirname = dirname(__filename);
7 |
8 | const compat = new FlatCompat({
9 | baseDirectory: __dirname,
10 | });
11 |
12 | const eslintConfig = [
13 | ...compat.extends("next/core-web-vitals", "next/typescript"),
14 | ];
15 |
16 | export default eslintConfig;
17 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 |
3 | export default {
4 | content: [
5 | "./pages/**/*.{js,ts,jsx,tsx,mdx}",
6 | "./components/**/*.{js,ts,jsx,tsx,mdx}",
7 | "./app/**/*.{js,ts,jsx,tsx,mdx}",
8 | ],
9 | theme: {
10 | extend: {
11 | colors: {
12 | background: "var(--background)",
13 | foreground: "var(--foreground)",
14 | },
15 | },
16 | },
17 | plugins: [],
18 | } satisfies Config;
19 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2017",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "module": "esnext",
11 | "moduleResolution": "bundler",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "jsx": "preserve",
15 | "incremental": true,
16 | "plugins": [
17 | {
18 | "name": "next"
19 | }
20 | ],
21 | "paths": {
22 | "@/*": ["./*"]
23 | }
24 | },
25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26 | "exclude": ["node_modules"]
27 | }
28 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.*
7 | .yarn/*
8 | !.yarn/patches
9 | !.yarn/plugins
10 | !.yarn/releases
11 | !.yarn/versions
12 |
13 | # lock files that are not pnpm
14 | package-lock.json
15 | yarn.lock
16 |
17 | # testing
18 | /coverage
19 |
20 | # next.js
21 | /.next/
22 | /out/
23 |
24 | # production
25 | /build
26 |
27 | # misc
28 | .DS_Store
29 | *.pem
30 |
31 | # debug
32 | npm-debug.log*
33 | yarn-debug.log*
34 | yarn-error.log*
35 | .pnpm-debug.log*
36 |
37 | # env files (can opt-in for committing if needed)
38 | .env*
39 |
40 | # vercel
41 | .vercel
42 |
43 | # typescript
44 | *.tsbuildinfo
45 | next-env.d.ts
46 |
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import { Geist, Geist_Mono } from "next/font/google";
3 | import "./globals.css";
4 |
5 | const geistSans = Geist({
6 | variable: "--font-geist-sans",
7 | subsets: ["latin"],
8 | });
9 |
10 | const geistMono = Geist_Mono({
11 | variable: "--font-geist-mono",
12 | subsets: ["latin"],
13 | });
14 |
15 | export const metadata: Metadata = {
16 | title: "Create Stagehand App",
17 | description: "Default starter kit for Stagehand",
18 | };
19 |
20 | export default function RootLayout({
21 | children,
22 | }: Readonly<{
23 | children: React.ReactNode;
24 | }>) {
25 | return (
26 |
27 |
30 | {children}
31 |
32 |
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/public/browserbase.svg:
--------------------------------------------------------------------------------
1 |
12 |
--------------------------------------------------------------------------------
/public/globe.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nextjs",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev --turbopack",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@browserbasehq/sdk": "^2.6.0",
13 | "@browserbasehq/stagehand": "^3.0.6",
14 | "dotenv": "^16.5.0",
15 | "next": "15.2.8",
16 | "playwright-core": "^1.57.0",
17 | "react": "19.0.1",
18 | "react-dom": "19.0.1",
19 | "zod": "^4.2.1"
20 | },
21 | "devDependencies": {
22 | "@eslint/eslintrc": "^3",
23 | "@types/node": "^20",
24 | "@types/react": "19.0.1",
25 | "@types/react-dom": "19.0.1",
26 | "eslint": "^9",
27 | "eslint-config-next": "15.1.6",
28 | "postcss": "^8",
29 | "tailwindcss": "^3.4.1",
30 | "typescript": "^5"
31 | },
32 | "packageManager": "pnpm@9.15.0+sha512.76e2379760a4328ec4415815bcd6628dee727af3779aaa4c914e3944156c4299921a89f976381ee107d41f12cfa4b66681ca9c718f0668fa0831ed4c6d8ba56c"
33 | }
34 |
--------------------------------------------------------------------------------
/public/browserbase_grayscale.svg:
--------------------------------------------------------------------------------
1 |
17 |
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/components/stagehand/debuggerIframe.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | export default function DebuggerIframe({
4 | debugUrl,
5 | env,
6 | }: {
7 | debugUrl?: string;
8 | env: "BROWSERBASE" | "LOCAL";
9 | }) {
10 | if (!debugUrl && env === "LOCAL") {
11 | return (
12 |
13 |
14 | Running in local mode.
15 |
16 | Set{" "}
17 |
18 | env: "BROWSERBASE"
19 |
20 | in{" "}
21 |
22 | stagehand.config.ts
23 | {" "}
24 | to see a live embedded browser.
25 |
26 |
27 | );
28 | }
29 |
30 | if (!debugUrl) {
31 | return (
32 |
33 |
34 | Loading...
35 |
36 |
37 | );
38 | }
39 |
40 | return (
41 |
48 | );
49 | }
50 |
--------------------------------------------------------------------------------
/app/api/stagehand/run.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 🤘 Welcome to Stagehand!
3 | *
4 | * This is the server-side entry point for Stagehand.
5 | *
6 | * To edit the Stagehand script, see `api/stagehand/main.ts`.
7 | * To edit config, see `stagehand.config.ts`.
8 | *
9 | * In this quickstart, we'll be automating a browser session to show you the power of Stagehand.
10 | */
11 | "use server";
12 |
13 | import StagehandConfig from "@/stagehand.config";
14 | import Browserbase from "@browserbasehq/sdk";
15 | import { Stagehand } from "@browserbasehq/stagehand";
16 | import { main } from "./main";
17 |
18 | export async function runStagehand(sessionId?: string) {
19 | const stagehand = new Stagehand({
20 | ...StagehandConfig,
21 | browserbaseSessionID: sessionId,
22 | });
23 | await stagehand.init();
24 | await main({ stagehand });
25 | await stagehand.close();
26 | }
27 |
28 | export async function startBBSSession() {
29 | const browserbase = new Browserbase(StagehandConfig);
30 | const session = await browserbase.sessions.create({
31 | projectId: StagehandConfig.projectId!,
32 | });
33 | const debugUrl = await browserbase.sessions.debug(session.id);
34 | return {
35 | sessionId: session.id,
36 | debugUrl: debugUrl.debuggerFullscreenUrl,
37 | };
38 | }
39 |
40 | export async function getConfig() {
41 | const hasBrowserbaseCredentials =
42 | process.env.BROWSERBASE_API_KEY !== undefined &&
43 | process.env.BROWSERBASE_PROJECT_ID !== undefined;
44 |
45 | const hasLLMCredentials = process.env.OPENAI_API_KEY !== undefined;
46 |
47 | return {
48 | env: StagehandConfig.env,
49 | verbose: StagehandConfig.verbose,
50 | domSettleTimeout: StagehandConfig.domSettleTimeout,
51 | browserbaseSessionID: StagehandConfig.browserbaseSessionID,
52 | hasBrowserbaseCredentials,
53 | hasLLMCredentials,
54 | };
55 | }
56 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 🤘 Welcome to Stagehand Next.js!
2 |
3 | [](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fbrowserbase%2Fstagehand-nextjs-quickstart&env=BROWSERBASE_API_KEY,BROWSERBASE_PROJECT_ID,OPENAI_API_KEY&envDescription=Browserbase%20credentials%20%2B%20OpenAI.%20You%20can%20configure%20your%20project%20to%20use%20Anthropic%20or%20a%20custom%20LLMClient%20in%20stagehand.config.ts&project-name=stagehand-nextjs&repository-name=stagehand-nextjs)
4 |
5 | Hey! This is a Next.js project built with [Stagehand](https://github.com/browserbase/stagehand).
6 |
7 | You can build your own web agent using: `npx create-browser-app`!
8 |
9 | ## Setting the Stage
10 |
11 | Stagehand is an SDK for automating browsers. It's built directly on top of [CDP](https://chromedevtools.github.io/devtools-protocol/) and provides a higher-level API for better debugging and AI fail-safes.
12 |
13 | ## Curtain Call
14 |
15 | Get ready for a show-stopping development experience. Just run:
16 |
17 | ```bash
18 | pnpm install && pnpm dev
19 | ```
20 |
21 | ## What's Next?
22 |
23 | ### Add your API keys
24 |
25 | This project defaults to using OpenAI, so it's going to throw a fit if you don't have an OpenAI API key.
26 |
27 | To use Anthropic (or other LLMs), you'll need to edit [stagehand.config.ts](stagehand.config.ts) to use the appropriate API key.
28 |
29 | You'll also want to set your Browserbase API key and project ID to run this project in the cloud.
30 |
31 | ```bash
32 | cp .example.env .env # Add your API keys to .env
33 | ```
34 |
35 | ### Custom .cursorrules
36 |
37 | We have custom .cursorrules for this project. It'll help quite a bit with writing Stagehand easily.
38 |
39 | ### Run on Browserbase
40 |
41 | To run on Browserbase, add your API keys to .env and change `env: "LOCAL"` to `env: "BROWSERBASE"` in [stagehand.config.ts](stagehand.config.ts).
42 |
43 | ### Use Anthropic Claude 4.5 Sonnet
44 |
45 | 1. Add your API key to .env
46 | 2. Change `modelName: "gpt-4o"` to `modelName: "claude-sonnet-4-5"` in [stagehand.config.ts](stagehand.config.ts)
47 | 3. Change `modelClientOptions: { apiKey: process.env.OPENAI_API_KEY }` to `modelClientOptions: { apiKey: process.env.ANTHROPIC_API_KEY }` in [stagehand.config.ts](stagehand.config.ts)
48 |
--------------------------------------------------------------------------------
/stagehand.config.ts:
--------------------------------------------------------------------------------
1 | import type { V3Options, LogLine } from "@browserbasehq/stagehand";
2 | import dotenv from "dotenv";
3 |
4 | dotenv.config();
5 |
6 | const StagehandConfig: V3Options = {
7 | env: "BROWSERBASE" /* Environment: "LOCAL" or "BROWSERBASE" */,
8 | verbose: 2 /* Logging verbosity: 0 (minimal), 1 (default), or 2 (detailed) */,
9 | apiKey: process.env.BROWSERBASE_API_KEY /* API key for authentication */,
10 | projectId: process.env.BROWSERBASE_PROJECT_ID /* Project identifier */,
11 | browserbaseSessionID:
12 | undefined /* Session ID for resuming Browserbase sessions */,
13 | browserbaseSessionCreateParams: {
14 | projectId: process.env.BROWSERBASE_PROJECT_ID!,
15 | } /* Parameters for creating Browserbase sessions */,
16 | localBrowserLaunchOptions: {
17 | headless: false,
18 | } /* Options for local browser launch (headless, args, etc.) */,
19 | model: {
20 | modelName: "gpt-4o",
21 | apiKey: process.env.OPENAI_API_KEY,
22 | } /* Model configuration: can be string or object with modelName and apiKey */,
23 | llmClient: undefined /* Optional: custom LLM client implementation */,
24 | systemPrompt: undefined /* Optional: system prompt for model */,
25 | logger: (message: LogLine) =>
26 | console.log(logLineToString(message)) /* Custom logging function */,
27 | domSettleTimeout: 30_000 /* Timeout for DOM to settle in milliseconds */,
28 | selfHeal: undefined /* Enable self-healing for failed actions */,
29 | logInferenceToFile: false /* Log inference calls to file */,
30 | experimental: false /* Enable experimental features */,
31 | disablePino: false /* Disable pino logging backend */,
32 | disableAPI: false /* Disable API functionality */,
33 | cacheDir: undefined /* Directory for caching actions (enables caching when set) */,
34 | };
35 | export default StagehandConfig;
36 |
37 | /**
38 | * Custom logging function that you can use to filter logs.
39 | *
40 | * General pattern here is that `message` will always be unique with no params
41 | * Any param you would put in a log is in `auxiliary`.
42 | *
43 | * For example, an error log looks like this:
44 | *
45 | * ```
46 | * {
47 | * category: "error",
48 | * message: "Some specific error occurred",
49 | * auxiliary: {
50 | * message: { value: "Error message", type: "string" },
51 | * trace: { value: "Error trace", type: "string" }
52 | * }
53 | * }
54 | * ```
55 | *
56 | * You can then use `logLineToString` to filter for a specific log pattern like
57 | *
58 | * ```
59 | * if (logLine.message === "Some specific error occurred") {
60 | * console.log(logLineToString(logLine));
61 | * }
62 | * ```
63 | */
64 | export function logLineToString(logLine: LogLine): string {
65 | // If you want more detail, set this to false. However, this will make the logs
66 | // more verbose and harder to read.
67 | const HIDE_AUXILIARY = true;
68 |
69 | try {
70 | const timestamp = logLine.timestamp || new Date().toISOString();
71 | if (logLine.auxiliary?.error) {
72 | return `${timestamp}::[stagehand:${logLine.category}] ${logLine.message}\n ${logLine.auxiliary.error.value}\n ${logLine.auxiliary.trace.value}`;
73 | }
74 |
75 | // If we want to hide auxiliary information, we don't add it to the log
76 | return `${timestamp}::[stagehand:${logLine.category}] ${logLine.message} ${
77 | logLine.auxiliary && !HIDE_AUXILIARY
78 | ? JSON.stringify(logLine.auxiliary)
79 | : ""
80 | }`;
81 | } catch (error) {
82 | console.error(`Error logging line:`, error);
83 | return "error logging line";
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/app/api/stagehand/main.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 🤘 Welcome to Stagehand!
3 | *
4 | *
5 | * To edit config, see `stagehand.config.ts`
6 | *
7 | * In this quickstart, we'll be automating a browser session to show you the power of Stagehand.
8 | *
9 | * 1. Go to https://docs.browserbase.com/
10 | * 2. Use `extract` to find information about the quickstart
11 | * 3. Use `observe` to find the links under the 'Guides' section
12 | * 4. Use Playwright to click the first link. If it fails, use `act` to gracefully fallback to Stagehand.
13 | */
14 |
15 | import { Stagehand } from "@browserbasehq/stagehand";
16 | import { chromium } from "playwright-core";
17 | import { z } from "zod/v3";
18 |
19 | export async function main({
20 | stagehand,
21 | }: {
22 | stagehand: Stagehand; // Stagehand instance
23 | }) {
24 | console.log(
25 | [
26 | `🤘 "Welcome to Stagehand!"`,
27 | "",
28 | "Stagehand is a tool that allows you to automate browser interactions.",
29 | "Watch as this demo automatically performs the following steps:",
30 | "",
31 | `📍 Step 1: Stagehand will auto-navigate to "https://docs.browserbase.com/"`,
32 | `📍 Step 2: Stagehand will use AI to "extract" information about the quickstart`,
33 | `📍 Step 3: Stagehand will use AI to "observe" and identify links in the 'Guides' section`,
34 | `📍 Step 4: Stagehand will attempt to click the first link using Playwright, with "act" as an AI fallback`,
35 | ].join("\n")
36 | );
37 |
38 | const page = stagehand.context.pages()[0];
39 | await page.goto("https://docs.browserbase.com/");
40 |
41 | // You can also attach Stagehand to Playwright and use those primitives directly
42 | const browser = await chromium.connectOverCDP({
43 | wsEndpoint: stagehand.connectURL(),
44 | });
45 |
46 | const pwContext = browser.contexts()[0];
47 | const pwPage = pwContext.pages()[0];
48 |
49 | // Zod is a schema validation library similar to Pydantic in Python
50 | // For more information on Zod, visit: https://zod.dev/
51 | const description = await stagehand.extract(
52 | "extract the title, description, and link of the quickstart",
53 | z.object({
54 | title: z.string(),
55 | link: z.string(),
56 | description: z.string(),
57 | })
58 | );
59 | announce(
60 | `The ${description.title} is at: ${description.link}` +
61 | `\n\n${description.description}` +
62 | `\n\n${JSON.stringify(description, null, 2)}`,
63 | "Extract"
64 | );
65 |
66 | const observeResult = await stagehand.observe(
67 | "Find the links under the 'Guides' section",
68 | );
69 | announce(
70 | `Observe: We can click:\n${observeResult
71 | .map((r) => `"${r.description}" -> ${r.selector}`)
72 | .join("\n")}`,
73 | "Observe"
74 | );
75 |
76 | try {
77 | throw new Error(
78 | "Comment out this error to run the base Playwright code!"
79 | );
80 |
81 | // Wait for search button and click it
82 | const quickStartSelector = `#content-area > div.relative.mt-8.prose.prose-gray.dark\:prose-invert > div > a:nth-child(1)`;
83 | await pwPage.waitForSelector(quickStartSelector);
84 | await pwPage.locator(quickStartSelector).click();
85 | await pwPage.waitForLoadState("networkidle");
86 | announce(
87 | `Clicked the quickstart link using base Playwright code. Uncomment line 118 in index.ts to have Stagehand take over!`
88 | );
89 | } catch (e) {
90 | if (!(e instanceof Error)) {
91 | throw e;
92 | }
93 |
94 | const actResult = await stagehand.act(
95 | "Click the link to the quickstart",
96 | );
97 | announce(
98 | `Clicked the quickstart link using Stagehand AI fallback.` +
99 | `\n${actResult}`,
100 | "Act"
101 | );
102 | }
103 |
104 | console.log(
105 | [
106 | "To recap, here are the steps we took:",
107 | `1. We went to https://docs.browserbase.com/`,
108 | `---`,
109 | `2. We used extract to find information about the quickstart`,
110 | `The ${description.title} is at: ${description.link}` +
111 | `\n\n${description.description}` +
112 | `\n\n${JSON.stringify(description, null, 2)}`,
113 | `---`,
114 | `3. We used observe to find the links under the 'Guides' section and got the following results:`,
115 | `We could have clicked:\n\n${observeResult
116 | .map((r) => `"${r.description}" -> ${r.selector}`)
117 | .join("\n")}`,
118 | `---`,
119 | `4. We used Playwright to click the first link. If it failed, we used act to gracefully fallback to Stagehand.`,
120 | ].join("\n\n")
121 | );
122 | }
123 |
124 | function announce(message: string, title?: string) {
125 | console.log({
126 | padding: 1,
127 | margin: 3,
128 | title: title || "Stagehand",
129 | message: message,
130 | });
131 | }
132 |
--------------------------------------------------------------------------------
/app/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import {
4 | getConfig,
5 | runStagehand,
6 | startBBSSession,
7 | } from "@/app/api/stagehand/run";
8 | import DebuggerIframe from "@/components/stagehand/debuggerIframe";
9 | import { V3Options } from "@browserbasehq/stagehand";
10 | import Image from "next/image";
11 | import { useCallback, useEffect, useState } from "react";
12 |
13 | export default function Home() {
14 | const [config, setConfig] = useState(null);
15 | const [running, setRunning] = useState(false);
16 | const [debugUrl, setDebugUrl] = useState(undefined);
17 | const [sessionId, setSessionId] = useState(undefined);
18 | const [error, setError] = useState(null);
19 | const [warning, setWarning] = useState(null);
20 |
21 | const fetchConfig = useCallback(async () => {
22 | const config = await getConfig();
23 | setConfig(config);
24 | const warningToShow: string[] = [];
25 | if (!config.hasLLMCredentials) {
26 | warningToShow.push(
27 | "No LLM credentials found. Edit stagehand.config.ts to configure your LLM client."
28 | );
29 | }
30 | if (!config.hasBrowserbaseCredentials) {
31 | warningToShow.push(
32 | "No BROWSERBASE_API_KEY or BROWSERBASE_PROJECT_ID found. You will probably want this to run Stagehand in the cloud."
33 | );
34 | }
35 | setWarning(warningToShow.join("\n"));
36 | }, []);
37 |
38 | const startScript = useCallback(async () => {
39 | if (!config) return;
40 |
41 | setRunning(true);
42 | setError(null);
43 |
44 | try {
45 | if (config.env === "BROWSERBASE") {
46 | const { sessionId, debugUrl } = await startBBSSession();
47 | setDebugUrl(debugUrl);
48 | setSessionId(sessionId);
49 | await runStagehand(sessionId);
50 | } else {
51 | await runStagehand();
52 | }
53 | } catch (error) {
54 | setError((error as Error).message);
55 | } finally {
56 | setRunning(false);
57 | }
58 | }, [config]);
59 |
60 | useEffect(() => {
61 | fetchConfig();
62 | }, [fetchConfig]);
63 |
64 | if (config === null) {
65 | return Loading...
;
66 | }
67 |
68 | return (
69 |
70 |
71 |
79 |
87 | {running && }
88 |
89 | -
90 | Get started by editing{" "}
91 |
92 | api/stagehand/main.ts
93 |
94 | .
95 |
96 |
97 |
98 |
146 | {error && (
147 |
148 | Error: {error}
149 |
150 | )}
151 | {warning && (
152 |
153 | Warning: {warning}
154 |
155 | )}
156 |
157 |
158 | );
159 | }
160 |
--------------------------------------------------------------------------------
/.cursorrules:
--------------------------------------------------------------------------------
1 | # Stagehand Project
2 |
3 | This is a project that uses Stagehand V3, a browser automation framework with AI-powered `act`, `extract`, `observe`, and `agent` methods.
4 |
5 | The main class can be imported as `Stagehand` from `@browserbasehq/stagehand`.
6 |
7 | **Key Classes:**
8 |
9 | - `Stagehand`: Main orchestrator class providing `act`, `extract`, `observe`, and `agent` methods
10 | - `context`: A `V3Context` object that manages browser contexts and pages
11 | - `page`: Individual page objects accessed via `stagehand.context.pages()[i]` or created with `stagehand.context.newPage()`
12 |
13 | ## Initialize
14 |
15 | ```typescript
16 | import { Stagehand } from "@browserbasehq/stagehand";
17 |
18 | const stagehand = new Stagehand({
19 | env: "LOCAL", // or "BROWSERBASE"
20 | verbose: 2, // 0, 1, or 2
21 | model: "openai/gpt-4.1-mini", // or any supported model
22 | });
23 |
24 | await stagehand.init();
25 |
26 | // Access the browser context and pages
27 | const page = stagehand.context.pages()[0];
28 | const context = stagehand.context;
29 |
30 | // Create new pages if needed
31 | const page2 = await stagehand.context.newPage();
32 | ```
33 |
34 | ## Act
35 |
36 | Actions are called on the `stagehand` instance (not the page). Use atomic, specific instructions:
37 |
38 | ```typescript
39 | // Act on the current active page
40 | await stagehand.act("click the sign in button");
41 |
42 | // Act on a specific page (when you need to target a page that isn't currently active)
43 | await stagehand.act("click the sign in button", { page: page2 });
44 | ```
45 |
46 | **Important:** Act instructions should be atomic and specific:
47 |
48 | - ✅ Good: "Click the sign in button" or "Type 'hello' into the search input"
49 | - ❌ Bad: "Order me pizza" or "Type in the search bar and hit enter" (multi-step)
50 |
51 | ### Observe + Act Pattern (Recommended)
52 |
53 | Cache the results of `observe` to avoid unexpected DOM changes:
54 |
55 | ```typescript
56 | const instruction = "Click the sign in button";
57 |
58 | // Get candidate actions
59 | const actions = await stagehand.observe(instruction);
60 |
61 | // Execute the first action
62 | await stagehand.act(actions[0]);
63 | ```
64 |
65 | To target a specific page:
66 |
67 | ```typescript
68 | const actions = await stagehand.observe("select blue as the favorite color", {
69 | page: page2,
70 | });
71 | await stagehand.act(actions[0], { page: page2 });
72 | ```
73 |
74 | ## Extract
75 |
76 | Extract data from pages using natural language instructions. The `extract` method is called on the `stagehand` instance.
77 |
78 | ### Basic Extraction (with schema)
79 |
80 | ```typescript
81 | import { z } from "zod/v3";
82 |
83 | // Extract with explicit schema
84 | const data = await stagehand.extract(
85 | "extract all apartment listings with prices and addresses",
86 | z.object({
87 | listings: z.array(
88 | z.object({
89 | price: z.string(),
90 | address: z.string(),
91 | }),
92 | ),
93 | }),
94 | );
95 |
96 | console.log(data.listings);
97 | ```
98 |
99 | ### Simple Extraction (without schema)
100 |
101 | ```typescript
102 | // Extract returns a default object with 'extraction' field
103 | const result = await stagehand.extract("extract the sign in button text");
104 |
105 | console.log(result);
106 | // Output: { extraction: "Sign in" }
107 |
108 | // Or destructure directly
109 | const { extraction } = await stagehand.extract(
110 | "extract the sign in button text",
111 | );
112 | console.log(extraction); // "Sign in"
113 | ```
114 |
115 | ### Targeted Extraction
116 |
117 | Extract data from a specific element using a selector:
118 |
119 | ```typescript
120 | const reason = await stagehand.extract(
121 | "extract the reason why script injection fails",
122 | z.string(),
123 | { selector: "/html/body/div[2]/div[3]/iframe/html/body/p[2]" },
124 | );
125 | ```
126 |
127 | ### URL Extraction
128 |
129 | When extracting links or URLs, use `z.string().url()`:
130 |
131 | ```typescript
132 | const { links } = await stagehand.extract(
133 | "extract all navigation links",
134 | z.object({
135 | links: z.array(z.string().url()),
136 | }),
137 | );
138 | ```
139 |
140 | ### Extracting from a Specific Page
141 |
142 | ```typescript
143 | // Extract from a specific page (when you need to target a page that isn't currently active)
144 | const data = await stagehand.extract(
145 | "extract the placeholder text on the name field",
146 | { page: page2 },
147 | );
148 | ```
149 |
150 | ## Observe
151 |
152 | Plan actions before executing them. Returns an array of candidate actions:
153 |
154 | ```typescript
155 | // Get candidate actions on the current active page
156 | const [action] = await stagehand.observe("Click the sign in button");
157 |
158 | // Execute the action
159 | await stagehand.act(action);
160 | ```
161 |
162 | Observing on a specific page:
163 |
164 | ```typescript
165 | // Target a specific page (when you need to target a page that isn't currently active)
166 | const actions = await stagehand.observe("find the next page button", {
167 | page: page2,
168 | });
169 | await stagehand.act(actions[0], { page: page2 });
170 | ```
171 |
172 | ## Agent
173 |
174 | Use the `agent` method to autonomously execute complex, multi-step tasks.
175 |
176 | ### Basic Agent Usage
177 |
178 | ```typescript
179 | const page = stagehand.context.pages()[0];
180 | await page.goto("https://www.google.com");
181 |
182 | const agent = stagehand.agent({
183 | model: "google/gemini-2.0-flash",
184 | executionModel: "google/gemini-2.0-flash",
185 | });
186 |
187 | const result = await agent.execute({
188 | instruction: "Search for the stock price of NVDA",
189 | maxSteps: 20,
190 | });
191 |
192 | console.log(result.message);
193 | ```
194 |
195 | ### Computer Use Agent (CUA)
196 |
197 | For more advanced scenarios using computer-use models:
198 |
199 | ```typescript
200 | const agent = stagehand.agent({
201 | cua: true, // Enable Computer Use Agent mode
202 | model: "anthropic/claude-sonnet-4-20250514",
203 | // or "google/gemini-2.5-computer-use-preview-10-2025"
204 | systemPrompt: `You are a helpful assistant that can use a web browser.
205 | Do not ask follow up questions, the user will trust your judgement.`,
206 | });
207 |
208 | await agent.execute({
209 | instruction: "Apply for a library card at the San Francisco Public Library",
210 | maxSteps: 30,
211 | });
212 | ```
213 |
214 | ### Agent with Custom Model Configuration
215 |
216 | ```typescript
217 | const agent = stagehand.agent({
218 | cua: true,
219 | model: {
220 | modelName: "google/gemini-2.5-computer-use-preview-10-2025",
221 | apiKey: process.env.GEMINI_API_KEY,
222 | },
223 | systemPrompt: `You are a helpful assistant.`,
224 | });
225 | ```
226 |
227 | ### Agent with Integrations (MCP/External Tools)
228 |
229 | ```typescript
230 | const agent = stagehand.agent({
231 | integrations: [`https://mcp.exa.ai/mcp?exaApiKey=${process.env.EXA_API_KEY}`],
232 | systemPrompt: `You have access to the Exa search tool.`,
233 | });
234 | ```
235 |
236 | ## Advanced Features
237 |
238 | ### DeepLocator (XPath Targeting)
239 |
240 | Target specific elements across shadow DOM and iframes:
241 |
242 | ```typescript
243 | await page
244 | .deepLocator("/html/body/div[2]/div[3]/iframe/html/body/p")
245 | .highlight({
246 | durationMs: 5000,
247 | contentColor: { r: 255, g: 0, b: 0 },
248 | });
249 | ```
250 |
251 | ### Multi-Page Workflows
252 |
253 | ```typescript
254 | const page1 = stagehand.context.pages()[0];
255 | await page1.goto("https://example.com");
256 |
257 | const page2 = await stagehand.context.newPage();
258 | await page2.goto("https://example2.com");
259 |
260 | // Act/extract/observe operate on the current active page by default
261 | // Pass { page } option to target a specific page
262 | await stagehand.act("click button", { page: page1 });
263 | await stagehand.extract("get title", { page: page2 });
264 | ```
--------------------------------------------------------------------------------
/public/logo_dark.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/logo_light.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------