├── .cursorrules ├── .example.env ├── .gitignore ├── README.md ├── app ├── api │ └── stagehand │ │ ├── main.ts │ │ └── run.ts ├── favicon.ico ├── globals.css ├── layout.tsx └── page.tsx ├── components └── stagehand │ └── debuggerIframe.tsx ├── eslint.config.mjs ├── next.config.ts ├── package.json ├── pnpm-lock.yaml ├── postcss.config.mjs ├── public ├── browserbase.svg ├── browserbase_grayscale.svg ├── file.svg ├── globe.svg ├── logo_dark.svg ├── logo_light.svg ├── next.svg ├── thumbnail.png ├── vercel.svg └── window.svg ├── stagehand.config.ts ├── tailwind.config.ts └── tsconfig.json /.cursorrules: -------------------------------------------------------------------------------- 1 | # Stagehand Project 2 | 3 | This is a project that uses Stagehand, which amplifies Playwright with `act`, `extract`, and `observe` added to the Page class. 4 | 5 | `Stagehand` is a class that provides config, a `StagehandPage` object via `stagehand.page`, and a `StagehandContext` object via `stagehand.context`. 6 | 7 | `Page` is a class that extends the Playwright `Page` class and adds `act`, `extract`, and `observe` methods. 8 | `Context` is a class that extends the Playwright `BrowserContext` class. 9 | 10 | Use the following rules to write code for this project. 11 | 12 | - When writing Playwright code, wrap it with Stagehand `act` 13 | - When writing code that needs to extract data from the page, use Stagehand `extract` 14 | - When writing code that needs to observe the page, use Stagehand `observe` 15 | 16 | ## Initialize 17 | 18 | ```typescript 19 | import { Stagehand } from "@browserbasehq/stagehand"; 20 | import StagehandConfig from "./stagehand.config"; 21 | 22 | const stagehand = new Stagehand(StagehandConfig); 23 | await stagehand.init(); 24 | 25 | const page = stagehand.page; // Playwright Page with act, extract, and observe methods 26 | const context = stagehand.context; // Playwright BrowserContext 27 | ``` 28 | 29 | ## Act 30 | 31 | For example, if you are writing Playwright code, wrap it with Stagehand `act` like this: 32 | 33 | ```typescript 34 | try { 35 | await page.locator('button[name="Sign in"]').click(); 36 | } catch (error) { 37 | await page.act({ 38 | action: "click the sign in button", 39 | }); 40 | } 41 | ``` 42 | 43 | Act `action` should be as atomic and specific as possible, i.e. "Click the sign in button" or "Type 'hello' into the search input". Avoid actions that are too broad, i.e. "Order me pizza" or "Send an email to Paul asking him to call me". Actions work best for Playwright code that is vulnerable to unexpected DOM changes. 44 | 45 | When using `act`, write Playwright code FIRST, then wrap it with a try-catch block where the catch block is `act`. 46 | 47 | ## Extract 48 | 49 | If you are writing code that needs to extract data from the page, use Stagehand `extract` like this: 50 | 51 | ```typescript 52 | const data = await page.extract({ 53 | instruction: "extract the sign in button text", 54 | schema: z.object({ 55 | text: z.string(), 56 | }), 57 | useTextExtract: true, 58 | }); 59 | ``` 60 | 61 | `schema` is a Zod schema that describes the data you want to extract. To extract an array, make sure to pass in a single object that contains the array, as follows: 62 | 63 | ```typescript 64 | const data = await page.extract({ 65 | instruction: "extract the text inside all buttons", 66 | schema: z.object({ 67 | text: z.array(z.string()), 68 | }), 69 | }); 70 | ``` 71 | 72 | Set `useTextExtract` to `true` for better results. 73 | 74 | ## Observe 75 | 76 | If you are writing code that needs to observe the page, use Stagehand `observe` like this: 77 | 78 | ```typescript 79 | const data = await page.observe({ 80 | instruction: "observe the page", 81 | }); 82 | ``` 83 | 84 | This returns a list of XPaths and descriptions of the data you want to extract as `{ selector: string; description: string }[]`. -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🤘 Welcome to Stagehand Next.js! 2 | 3 | [![Deploy with Vercel](https://vercel.com/button)](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 on top of [Playwright](https://playwright.dev/) 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 | npm install && npm run 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 3.5 Sonnet 44 | 45 | 1. Add your API key to .env 46 | 2. Change `modelName: "gpt-4o"` to `modelName: "claude-3-5-sonnet-latest"` 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 | -------------------------------------------------------------------------------- /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 Playwright and Stagehand's AI features. 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 AI. 13 | */ 14 | 15 | import { Page, BrowserContext, Stagehand } from "@browserbasehq/stagehand"; 16 | import { z } from "zod"; 17 | 18 | export async function main({ 19 | page, 20 | stagehand, 21 | }: { 22 | page: Page; // Playwright Page with act, extract, and observe methods 23 | context: BrowserContext; // Playwright BrowserContext 24 | stagehand: Stagehand; // Stagehand instance 25 | }) { 26 | console.log( 27 | [ 28 | `🤘 "Welcome to Stagehand!"`, 29 | "", 30 | "Stagehand is a tool that allows you to automate browser interactions.", 31 | "Watch as this demo automatically performs the following steps:", 32 | "", 33 | `📍 Step 1: Stagehand will auto-navigate to "https://docs.browserbase.com/"`, 34 | `📍 Step 2: Stagehand will use AI to "extract" information about the quickstart`, 35 | `📍 Step 3: Stagehand will use AI to "observe" and identify links in the 'Guides' section`, 36 | `📍 Step 4: Stagehand will attempt to click the first link using Playwright, with "act" as an AI fallback`, 37 | ].join("\n") 38 | ); 39 | 40 | // You can use the `page` instance to write any Playwright code 41 | // For more info: https://playwright.dev/docs/pom 42 | await page.goto("https://docs.browserbase.com/"); 43 | 44 | const description = await page.extract({ 45 | instruction: "extract the title, description, and link of the quickstart", 46 | // Zod is a schema validation library similar to Pydantic in Python 47 | // For more information on Zod, visit: https://zod.dev/ 48 | schema: z.object({ 49 | title: z.string(), 50 | link: z.string(), 51 | description: z.string(), 52 | }), 53 | }); 54 | announce( 55 | `The ${description.title} is at: ${description.link}` + 56 | `\n\n${description.description}` + 57 | `\n\n${JSON.stringify(description, null, 2)}`, 58 | "Extract" 59 | ); 60 | 61 | const observeResult = await page.observe({ 62 | instruction: "Find the links under the 'Guides' section", 63 | }); 64 | announce( 65 | `Observe: We can click:\n${observeResult 66 | .map((r) => `"${r.description}" -> ${r.selector}`) 67 | .join("\n")}`, 68 | "Observe" 69 | ); 70 | 71 | // In the event that your Playwright code fails, you can use the `act` method to 72 | // let Stagehand AI take over and complete the action. 73 | try { 74 | throw new Error( 75 | "Comment out line 118 in index.ts to run the base Playwright code!" 76 | ); 77 | 78 | // Wait for search button and click it 79 | const quickStartSelector = `#content-area > div.relative.mt-8.prose.prose-gray.dark\:prose-invert > div > a:nth-child(1)`; 80 | await page.waitForSelector(quickStartSelector); 81 | await page.locator(quickStartSelector).click(); 82 | await page.waitForLoadState("networkidle"); 83 | announce( 84 | `Clicked the quickstart link using base Playwright code. Uncomment line 118 in index.ts to have Stagehand take over!` 85 | ); 86 | } catch (e) { 87 | if (!(e instanceof Error)) { 88 | throw e; 89 | } 90 | announce( 91 | `Looks like an error occurred running Playwright. Let's have Stagehand take over!` + 92 | `\n${e.message}`, 93 | "Playwright" 94 | ); 95 | 96 | const actResult = await page.act({ 97 | action: "Click the link to the quickstart", 98 | }); 99 | announce( 100 | `Clicked the quickstart link using Stagehand AI fallback.` + 101 | `\n${actResult}`, 102 | "Act" 103 | ); 104 | } 105 | 106 | // Close the browser 107 | await stagehand.close(); 108 | 109 | console.log( 110 | [ 111 | "To recap, here are the steps we took:", 112 | `1. We went to https://docs.browserbase.com/`, 113 | `---`, 114 | `2. We used extract to find information about the quickstart`, 115 | `The ${description.title} is at: ${description.link}` + 116 | `\n\n${description.description}` + 117 | `\n\n${JSON.stringify(description, null, 2)}`, 118 | `---`, 119 | `3. We used observe to find the links under the 'Guides' section and got the following results:`, 120 | `We could have clicked:\n\n${observeResult 121 | .map((r) => `"${r.description}" -> ${r.selector}`) 122 | .join("\n")}`, 123 | `---`, 124 | `4. We used Playwright to click the first link. If it failed, we used act to gracefully fallback to Stagehand AI.`, 125 | ].join("\n\n") 126 | ); 127 | } 128 | 129 | function announce(message: string, title?: string) { 130 | console.log({ 131 | padding: 1, 132 | margin: 3, 133 | title: title || "Stagehand", 134 | }); 135 | } 136 | -------------------------------------------------------------------------------- /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 Playwright and Stagehand's AI features. 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({ page: stagehand.page, context: stagehand.context, 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 | debugDom: StagehandConfig.debugDom, 50 | headless: StagehandConfig.headless, 51 | domSettleTimeoutMs: StagehandConfig.domSettleTimeoutMs, 52 | browserbaseSessionID: StagehandConfig.browserbaseSessionID, 53 | hasBrowserbaseCredentials, 54 | hasLLMCredentials, 55 | }; 56 | } 57 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/browserbase/stagehand-nextjs-quickstart/f331a065cd1bf5e78502a4392453ef61468fae6d/app/favicon.ico -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 { ConstructorParams } 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 | 43 | try { 44 | if (config.env === "BROWSERBASE") { 45 | const { sessionId, debugUrl } = await startBBSSession(); 46 | setDebugUrl(debugUrl); 47 | setSessionId(sessionId); 48 | await runStagehand(sessionId); 49 | } else { 50 | await runStagehand(); 51 | } 52 | } catch (error) { 53 | setError((error as Error).message); 54 | } finally { 55 | setRunning(false); 56 | } 57 | }, [config]); 58 | 59 | useEffect(() => { 60 | fetchConfig(); 61 | }, [fetchConfig]); 62 | 63 | if (config === null) { 64 | return
Loading...
; 65 | } 66 | 67 | return ( 68 |
69 |
70 | Stagehand logo 78 | Stagehand logo 86 | {running && } 87 |
    88 |
  • 89 | Get started by editing{" "} 90 | 91 | api/stagehand/main.ts 92 | 93 | . 94 |
  • 95 |
96 | 97 | 142 | {error && ( 143 |
144 | Error: {error} 145 |
146 | )} 147 | {warning && ( 148 |
149 | Warning: {warning} 150 |
151 | )} 152 |
153 |
154 | ); 155 | } 156 | -------------------------------------------------------------------------------- /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