> ## Documentation Index
> Fetch the complete documentation index at: https://docs.browserbase.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Build a research agent with Stagehand + Vercel

> Build an AI-powered research agent that runs parallel browser sessions on Browserbase, powered by Stagehand and the Vercel AI SDK.

<Tabs>
  <Tab title="Stagehand + Vercel AI SDK">
    <Steps titleSize="h3">
      <Step title="Get your API keys">
        You'll need credentials from two services:

        **Browserbase** — Go to the [Dashboard Settings](https://www.browserbase.com/settings) and copy your API key.

        <Frame>
          <img src="https://mintcdn.com/browserbase/giE_cpy18f2mWHqr/images/quickstart/api_key.png?fit=max&auto=format&n=giE_cpy18f2mWHqr&q=85&s=4ac94a8f69cec20bd17b2a8788169062" width="3410" height="1864" data-path="images/quickstart/api_key.png" />
        </Frame>

        **Anthropic** — Get your API key from the [Anthropic Console](https://console.anthropic.com/).

        Create a `.env.local` file with:

        ```bash theme={null}
        BROWSERBASE_API_KEY=your_api_key
        ANTHROPIC_API_KEY=your_anthropic_key
        ```
      </Step>

      <Step title="Install dependencies">
        ```bash theme={null}
        npm i @browserbasehq/stagehand @browserbasehq/sdk ai @ai-sdk/anthropic zod
        ```

        | Package                    | Purpose                                       |
        | -------------------------- | --------------------------------------------- |
        | `@browserbasehq/stagehand` | AI-powered browser automation                 |
        | `@browserbasehq/sdk`       | Browserbase API client (sessions, live views) |
        | `ai`                       | Vercel AI SDK for structured generation       |
        | `@ai-sdk/anthropic`        | Anthropic model provider                      |
        | `zod`                      | Schema validation for extracted data          |
      </Step>

      <Step title="Create a Stagehand session">
        Initialize a Stagehand instance connected to Browserbase. Each session gets its own cloud browser with a live debug view.

        ```ts theme={null}
        import { Stagehand } from "@browserbasehq/stagehand";
        import Browserbase from "@browserbasehq/sdk";

        const browserbase = new Browserbase();

        async function createStagehandSession(source: string) {
          const stagehand = new Stagehand({
            env: "BROWSERBASE",
            model: "anthropic/claude-sonnet-4-6",
            logger: console.log,
            disablePino: true,
          });

          await stagehand.init();

          const sessionId = stagehand.browserbaseSessionID!;
          const { debuggerFullscreenUrl } = await browserbase.sessions.debug(sessionId);

          return { stagehand, sessionId, liveViewUrl: debuggerFullscreenUrl, source };
        }
        ```
      </Step>

      <Step title="Define research functions">
        Each research function takes a Stagehand instance, navigates to a source, and uses `stagehand.extract()` to pull structured data from the page using AI.

        Here's an example that searches DuckDuckGo and visits top results:

        ```ts theme={null}
        import { z } from "zod";

        async function researchGoogle(
          stagehand: Stagehand,
          query: string,
          onFinding: (finding: Finding) => void
        ) {
          const page = stagehand.context.activePage()!;

          await page.goto(`https://duckduckgo.com/?q=${encodeURIComponent(query)}`);
          await page.waitForTimeout(2000);

          const searchResults = await stagehand.extract(
            "Extract the top 5 organic search result links with their titles and URLs. Skip any ads.",
            z.object({
              results: z.array(z.object({
                title: z.string(),
                url: z.string(),
              })).max(5),
            })
          );

          for (const result of searchResults.results.slice(0, 3)) {
            if (!result.url || result.url.includes("duckduckgo.com")) continue;

            await page.goto(result.url, { waitUntil: "domcontentloaded", timeoutMs: 15000 });

            const content = await stagehand.extract(
              `Extract the key information about "${query}" from this article.`,
              z.object({
                summary: z.string(),
                keyFacts: z.array(z.string()),
              })
            );

            if (content.summary) {
              onFinding({
                title: result.title,
                source: new URL(result.url).hostname.replace("www.", ""),
                url: result.url,
                summary: content.summary,
                relevance: "high",
              });
            }
          }
        }
        ```

        You can create similar functions for Wikipedia, YouTube, Hacker News, and Google News — each using `stagehand.extract()` with different schemas. See the [full template](https://github.com/browserbase/browserbase-nextjs-template/blob/main/app/api/research/route.ts) for all five research functions.
      </Step>

      <Step title="Create the API route with SSE streaming">
        Create `app/api/research/route.ts` to handle research requests. This route creates parallel Stagehand sessions and streams findings back via Server-Sent Events.

        ```ts theme={null}
        import { generateObject } from "ai";
        import { anthropic } from "@ai-sdk/anthropic";

        export const maxDuration = 300;

        const ResearchSummarySchema = z.object({
          overview: z.string().describe("2-3 sentence direct answer to the query"),
          keyFacts: z.array(z.string()).describe("3-6 specific facts with dates, numbers, or names"),
          recentDevelopments: z.string().nullable().describe("Latest news if applicable"),
          sourcesSummary: z.string().describe("Brief note on the types of sources consulted"),
        });

        export async function POST(req: Request) {
          const { query } = await req.json();

          const encoder = new TextEncoder();
          const stream = new TransformStream();
          const writer = stream.writable.getWriter();

          const sendEvent = async (event: string, data: unknown) => {
            await writer.write(
              encoder.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`)
            );
          };

          (async () => {
            const sessions = [];
            const allFindings: Finding[] = [];

            const researchFunctions = [
              { source: "News", fn: researchGoogleNews },
              { source: "Hacker News", fn: researchHackerNews },
              { source: "YouTube", fn: researchYouTube },
              { source: "Wikipedia", fn: researchWikipedia },
              { source: "Search", fn: researchGoogle },
            ];

            try {
              await sendEvent("status", { message: "Starting browser sessions...", phase: "init" });

              // Create all Stagehand sessions in parallel
              const sessionPromises = researchFunctions.map(({ source }) =>
                createStagehandSession(source)
              );
              const createdSessions = await Promise.all(sessionPromises);
              sessions.push(...createdSessions);

              // Send live view URLs to frontend
              await sendEvent("liveViews", {
                sessions: sessions.map(s => ({
                  source: s.source,
                  liveViewUrl: s.liveViewUrl,
                  sessionId: s.sessionId,
                })),
              });

              // Run all research in parallel
              await Promise.allSettled(
                researchFunctions.map(({ source, fn }, index) =>
                  fn(
                    sessions[index].stagehand,
                    query,
                    (finding) => {
                      allFindings.push(finding);
                      sendEvent("findings", { findings: allFindings });
                    }
                  )
                )
              );

              // Synthesize findings with AI
              if (allFindings.length > 0) {
                const findingsText = allFindings
                  .map((f) => `Source: ${f.source}\n${f.summary}`)
                  .join("\n\n---\n\n");

                const { object: summary } = await generateObject({
                  model: anthropic("claude-sonnet-4-6"),
                  schema: ResearchSummarySchema,
                  prompt: `Based on these research findings about "${query}", create a structured summary.\n\n${findingsText}`,
                });

                await sendEvent("complete", { findings: allFindings, summary });
              }
            } finally {
              for (const session of sessions) {
                try { await session.stagehand.close(); } catch {}
              }
              await writer.close();
            }
          })();

          return new Response(stream.readable, {
            headers: {
              "Content-Type": "text/event-stream",
              "Cache-Control": "no-cache",
              Connection: "keep-alive",
            },
          });
        }
        ```
      </Step>

      <Step title="Handle concurrency limits">
        Free Browserbase plans have a concurrency limit of 1. The template automatically detects this and falls back to running sessions sequentially:

        ```ts theme={null}
        async function getProjectConcurrency(): Promise<number> {
          const projects = await browserbase.projects.list();
          if (!projects?.length) return 1;
          const project = await browserbase.projects.retrieve(projects[0].id);
          return project.concurrency ?? 1;
        }

        // In your POST handler:
        const concurrency = await getProjectConcurrency();

        if (concurrency === 1) {
          // Run browsers one at a time, closing each before starting the next
          for (const { source, fn } of researchFunctions) {
            const session = await createStagehandSession(source);
            await fn(session.stagehand, query, onFinding);
            await session.stagehand.close();
          }
        } else {
          // Run all browsers in parallel
          const sessions = await Promise.all(
            researchFunctions.map(({ source }) => createStagehandSession(source))
          );
          await Promise.allSettled(
            researchFunctions.map(({ fn }, i) =>
              fn(sessions[i].stagehand, query, onFinding)
            )
          );
        }
        ```
      </Step>
    </Steps>

    Congratulations! You've built an AI research agent that runs parallel browser sessions with Stagehand and Browserbase on Vercel.

    For the complete implementation including the frontend UI with live browser views, check out the full template:

    <CardGroup cols={2}>
      <Card title="Full Template on GitHub" icon="github" iconType="sharp-solid" href="https://github.com/browserbase/browserbase-nextjs-template">
        Browse the complete source code with frontend components, SSE streaming, and live browser views.
      </Card>

      <Card title="Deploy to Vercel" icon="rocket" iconType="sharp-solid" href="https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fbrowserbase%2Fbrowserbase-nextjs-template&stores=%5B%7B%22type%22%3A%22integration%22%2C%22protocol%22%3A%22other%22%2C%22productSlug%22%3A%22browserbase%22%2C%22integrationSlug%22%3A%22browserbase%22%7D%5D">
        One-click deploy with automatic Browserbase setup via the Vercel Marketplace.
      </Card>
    </CardGroup>
  </Tab>
</Tabs>
