Skip to main content
The fastest path is the finished Browserbase example: clone it and run two commands. The sections after it explain how the example is built so you can adapt it. New to devboxes? Start with the Quickstart.
Give a Runloop agent browser access with Browserbase: the agent runs in a devbox, the browser runs on Browserbase, and the devbox drives it by connecting Playwright over CDP to the session’s connect URL, so no Chromium ever runs in the devbox. In this guide:

What you need

  • A Runloop API key
  • A Browserbase API key and project ID
  • Python 3.12+ and pip, or Node.js 18+ and npm

Environment variables

export RUNLOOP_API_KEY=...
export BROWSERBASE_API_KEY=...
export BROWSERBASE_PROJECT_ID=...
Instead of exporting the Browserbase keys, store them as Runloop account secrets and map them into the devbox at runtime. See Account Secrets.

Run the finished example

Clone the repo, install dependencies, then create the blueprint and run the browser task.
git clone https://github.com/runloopai/runloop-examples.git
cd runloop-examples/browser-integrations/browserbase/python
pip install -r requirements.txt
python main.py create-blueprint
python main.py run
git clone https://github.com/runloopai/runloop-examples.git
cd runloop-examples/browser-integrations/browserbase/typescript
npm install
npm run create-blueprint
npm run run-browserbase
create-blueprint bakes the Browserbase SDK and the Playwright client into a reusable blueprint, and run creates a devbox from it, uploads the agent, and drives a Browserbase browser. The rest of this guide walks through each of those pieces.

How the example is built

To build it yourself, install the Runloop SDK locally. The devbox installs the Browserbase SDK and Playwright client itself (baked into the blueprint below), so nothing else is needed on your machine.
pip install runloop_api_client
npm install @runloop/api-client

Create a blueprint with the Browserbase SDK

Bake the Browserbase SDK and the Playwright client into a blueprint once so every devbox starts ready, with no install step. There is no playwright install chromium: the browser runs on Browserbase, so the devbox needs only the Playwright client library.
from runloop_api_client import AsyncRunloopSDK

runloop = AsyncRunloopSDK()

blueprint = await runloop.blueprint.create(
    name="browserbase-browser",
    system_setup_commands=["python3 -m pip install --user browserbase playwright"],
)
import { RunloopSDK } from '@runloop/api-client';

const runloop = new RunloopSDK();

const blueprint = await runloop.blueprint.create({
  name: 'browserbase-browser',
  system_setup_commands: ['python3 -m pip install --user browserbase playwright'],
});
From the example: python main.py create-blueprint or npm run create-blueprint. It is idempotent, so later runs reuse the built blueprint.

Create a devbox and run a browser task

Create a devbox from the blueprint with the Browserbase keys injected, then have it create a Browserbase browser and drive it with Playwright over CDP. The agent connects to session.connect_url and runs ordinary Playwright code against the remote browser. Here the agent returns structured JSON (title, headings, and link count) rather than a single string, which is the first useful browser primitive. The TypeScript version below orchestrates Runloop from Node, but the in-devbox agent is still Python, so the blueprint only needs the Python Browserbase SDK.
import os

agent = '''
import asyncio
import os
from browserbase import AsyncBrowserbase
from playwright.async_api import async_playwright

async def main():
    bb = AsyncBrowserbase()
    project_id = os.environ["BROWSERBASE_PROJECT_ID"]
    session = await bb.sessions.create(project_id=project_id)
    try:
        async with async_playwright() as pw:
            browser = await pw.chromium.connect_over_cdp(session.connect_url)
            try:
                ctx = browser.contexts[0] if browser.contexts else await browser.new_context()
                page = ctx.pages[0] if ctx.pages else await ctx.new_page()
                await page.goto("https://docs.runloop.ai", wait_until="domcontentloaded")
                data = await page.evaluate("""() => ({
                  title: document.title,
                  headings: document.querySelectorAll("h1, h2, h3").length,
                  links: document.querySelectorAll("a[href]").length,
                })""")
                print(data)
            finally:
                await browser.close()
    finally:
        await bb.sessions.update(session.id, status="REQUEST_RELEASE", project_id=project_id)

asyncio.run(main())
'''

devbox = await runloop.devbox.create_from_blueprint_name(
    "browserbase-browser",
    environment_variables={
        "BROWSERBASE_API_KEY": os.environ["BROWSERBASE_API_KEY"],
        "BROWSERBASE_PROJECT_ID": os.environ["BROWSERBASE_PROJECT_ID"],
    },
    launch_parameters={"resource_size_request": "SMALL"},
)
try:
    await devbox.file.write(file_path="/home/user/agent.py", contents=agent)
    result = await devbox.cmd.exec("python3 /home/user/agent.py")
    print(await result.stdout())
finally:
    await devbox.shutdown()
const agent = `
import asyncio
import os
from browserbase import AsyncBrowserbase
from playwright.async_api import async_playwright

async def main():
    bb = AsyncBrowserbase()
    project_id = os.environ["BROWSERBASE_PROJECT_ID"]
    session = await bb.sessions.create(project_id=project_id)
    try:
        async with async_playwright() as pw:
            browser = await pw.chromium.connect_over_cdp(session.connect_url)
            try:
                ctx = browser.contexts[0] if browser.contexts else await browser.new_context()
                page = ctx.pages[0] if ctx.pages else await ctx.new_page()
                await page.goto("https://docs.runloop.ai", wait_until="domcontentloaded")
                data = await page.evaluate("""() => ({
                  title: document.title,
                  headings: document.querySelectorAll("h1, h2, h3").length,
                  links: document.querySelectorAll("a[href]").length,
                })""")
                print(data)
            finally:
                await browser.close()
    finally:
        await bb.sessions.update(session.id, status="REQUEST_RELEASE", project_id=project_id)

asyncio.run(main())
`;

const devbox = await runloop.devbox.createFromBlueprintName('browserbase-browser', {
  environment_variables: {
    BROWSERBASE_API_KEY: process.env.BROWSERBASE_API_KEY!,
    BROWSERBASE_PROJECT_ID: process.env.BROWSERBASE_PROJECT_ID!,
  },
  launch_parameters: { resource_size_request: 'SMALL' },
});

try {
  await devbox.file.write({ file_path: '/home/user/agent.py', contents: agent });
  const result = await devbox.cmd.exec('python3 /home/user/agent.py');
  console.log(await result.stdout());
} finally {
  await devbox.shutdown();
}
The Browserbase SDK and the Playwright client are the only dependencies the devbox needs. There is no Chromium install, because the browser runs on Browserbase and connect_over_cdp drives it remotely.
From the example: python main.py run or npm run run-browserbase.

Research across multiple pages

To research several pages, reuse one Browserbase session: it avoids creating a session per page and preserves cookies and history. Connect Playwright once, define a reusable scan function that navigates and returns clean JSON, then call it for each URL. This code goes in the same in-devbox agent shown above.
import os
from browserbase import AsyncBrowserbase
from playwright.async_api import async_playwright

bb = AsyncBrowserbase()
session = await bb.sessions.create(project_id=os.environ["BROWSERBASE_PROJECT_ID"])
try:
    async with async_playwright() as pw:
        browser = await pw.chromium.connect_over_cdp(session.connect_url)
        page = browser.contexts[0].pages[0]

        async def scan(url):
            await page.goto(url, wait_until="domcontentloaded", timeout=30000)
            return await page.evaluate("""() => {
              const clean = (s) => (s || "").replace(/\\s+/g, " ").trim();
              return {
                title: clean(document.title),
                headings: Array.from(document.querySelectorAll("h1, h2, h3"))
                  .map((e) => clean(e.textContent)).filter(Boolean).slice(0, 12),
                links: document.querySelectorAll("a[href]").length,
              };
            }""")

        try:
            for url in ["https://runloop.ai", "https://docs.runloop.ai"]:
                data = await scan(url)
                print(data["title"], len(data["headings"]), "headings")
        finally:
            await browser.close()
finally:
    await bb.sessions.update(
        session.id, status="REQUEST_RELEASE", project_id=os.environ["BROWSERBASE_PROJECT_ID"]
    )
import { chromium } from 'playwright-core';
import Browserbase from '@browserbasehq/sdk';

const bb = new Browserbase({ apiKey: process.env.BROWSERBASE_API_KEY });
const session = await bb.sessions.create({ projectId: process.env.BROWSERBASE_PROJECT_ID! });
try {
  const browser = await chromium.connectOverCDP(session.connectUrl);
  const page = browser.contexts()[0].pages()[0];

  async function scan(url: string) {
    await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
    return page.evaluate(() => {
      const clean = (s: string) => (s || '').replace(/\s+/g, ' ').trim();
      return {
        title: clean(document.title),
        headings: Array.from(document.querySelectorAll('h1, h2, h3'))
          .map((e) => clean(e.textContent ?? '')).filter(Boolean).slice(0, 12),
        links: document.querySelectorAll('a[href]').length,
      };
    });
  }

  try {
    for (const url of ['https://runloop.ai', 'https://docs.runloop.ai']) {
      const data = await scan(url);
      console.log(data.title, data.headings.length, 'headings');
    }
  } finally {
    await browser.close();
  }
} finally {
  await bb.sessions.update(session.id, {
    status: 'REQUEST_RELEASE',
    projectId: process.env.BROWSERBASE_PROJECT_ID!,
  });
}
The TypeScript snippets connect with playwright-core, which has no bundled browser. That is the point: the browser runs on Browserbase, so the client needs only the Playwright protocol, not a local Chromium.

AI actions with Stagehand

When an agent should not hard-code selectors, use Stagehand, Browserbase’s AI browser-automation framework. You describe an action in natural language and a model resolves it against the live page at runtime. The primitives are act (do something), extract (pull typed data), and observe (see what’s actionable). Stagehand runs on Browserbase and needs a model API key in addition to your Browserbase keys.
import os
from stagehand import Stagehand

# The `stagehand` PyPI package is the REST SDK: you call client.sessions.*.
client = Stagehand(
    browserbase_api_key=os.environ["BROWSERBASE_API_KEY"],
    model_api_key=os.environ["MODEL_API_KEY"],
    server="remote",
)

started = client.sessions.start(model_name="openai/gpt-4.1-mini", browser={"type": "browserbase"})
session_id = started.data.session_id
try:
    client.sessions.navigate(session_id, url="https://docs.browserbase.com")
    client.sessions.act(session_id, input="click the link to the quickstart guide")
    result = client.sessions.extract(
        session_id,
        instruction="extract the page title and first heading",
        schema={
            "type": "object",
            "properties": {"title": {"type": "string"}, "heading": {"type": "string"}},
            "required": ["title", "heading"],
        },
    )
    print(result.data)
finally:
    client.sessions.end(session_id)
// Stagehand v3: act/extract live on the instance; page comes from the context.
import { Stagehand } from '@browserbasehq/stagehand';
import { z } from 'zod';

const stagehand = new Stagehand({
  env: 'BROWSERBASE',
  apiKey: process.env.BROWSERBASE_API_KEY,
  projectId: process.env.BROWSERBASE_PROJECT_ID,
  model: 'openai/gpt-4.1-mini',
});

await stagehand.init();
const page = stagehand.context.pages()[0];
try {
  await page.goto('https://docs.browserbase.com');
  await stagehand.act('click the link to the quickstart guide');
  const result = await stagehand.extract(
    'extract the page title and first heading',
    z.object({ title: z.string(), heading: z.string() }),
  );
  console.log(result);
} finally {
  await stagehand.close();
}
Stagehand runs the browser automation on Browserbase, so the model issues fewer round-trips than thousands of individual CDP calls. It is the lower-chatter path for high-volume, multi-step flows.

How the integration works internally

  1. A devbox boots from a blueprint with the Browserbase SDK and Playwright client already installed.
  2. BROWSERBASE_API_KEY and BROWSERBASE_PROJECT_ID are injected into the devbox environment.
  3. The agent calls sessions.create() to get a Browserbase cloud browser session.
  4. It connects Playwright to the session with chromium.connect_over_cdp(session.connect_url) and drives the remote browser. No local Chromium.
  5. Results return to the devbox; the orchestrator reads them and shuts the devbox down. The session is released with sessions.update(..., status="REQUEST_RELEASE").

Common issues

  • resource_size_request rejected
    • The enum is upper-case (SMALL, MEDIUM, LARGE, …). Lower-case values return a 400.
  • Browserbase auth errors inside the devbox
    • Confirm both BROWSERBASE_API_KEY and BROWSERBASE_PROJECT_ID were passed via environment_variables when creating the devbox.
  • connect_over_cdp cannot connect
    • Use session.connect_url from a freshly created session. A session that has timed out or been released will refuse the connection.
  • Advanced stealth or proxies rejected
    • Proxies need the Developer plan; advanced stealth and verified mode need a top-tier plan. The API returns a 403 on lower plans; degrade to a plain session.

Next Steps