Browser tools

browserTools() and browserSubagent() from @stackbone/sdk/deep give a deep agent a Chromium browser. The agent navigates to a page, reads what is on it, acts on it in plain language, pulls typed data out of it, and screenshots it. You declare the capability in one line and the SDK builds the tools for you. Each call runs against a Chromium page the runtime keeps alive for the session, so a login survives from one turn to the next.

connectorTool reaches a provider's API (see Stackbone Connect). The browser tools reach a page a human would open in a browser.

What you get

browserTools() returns six tools with fixed, model-facing names. Four of them use a model to interpret the page; two are plain navigation and capture.

Tool What it does Uses a model?
browser_goto Navigate to an absolute URL and load the page. No
browser_observe List the actions on the current page (buttons, links, inputs). Yes
browser_act Do a natural-language action, like "click the login button". Yes
browser_extract Pull structured, typed data off the page instead of raw HTML. Yes
browser_screenshot Capture the page as a base64 PNG. No
browser_login Sign in to a site once and keep the session for later runs. Yes

Every tool returns a small JSON string the model can read. Success looks like { "ok": true, ... } and failure looks like { "ok": false, "error": "..." }. A tool never throws in a way that crashes the run, so the model always gets a result it can reason about.

Stay signed in across runs

A fresh browser starts logged out of every site. browser_login(url) opens the sign-in page, submits the operator's stored credentials, and saves the resulting session (cookies and local storage) encrypted, keyed by site. Later browsers start with that session already injected, so the next conversation begins authenticated instead of logging in again.

The credentials come from secrets named after the site host, so you never type a password into chat. For https://app.example.com the agent reads BROWSER_LOGIN_APP_EXAMPLE_COM_USERNAME and BROWSER_LOGIN_APP_EXAMPLE_COM_PASSWORD. Set them in Studio under Secrets. If either is missing, browser_login returns an error naming the secrets to add.

You audit and revoke saved sessions in Studio under Browser settings, on the Profiles tab. A saved session is as good as a password, so treat a revoke like a password rotation. If a site expires the session server-side, the agent notices, marks it stale, and signs in once more on the next browser_login.

Two conversations that hit the same site at once do not both log in. One performs the login while the other waits and inherits the saved session. If the site demands a step the agent cannot complete, such as a one-time code or a captcha, browser_login says so instead of looping.

Install the browser peers

The browser tools drive Stagehand over a real Chromium page. The SDK never imports Stagehand or Playwright on its own. They are optional dependencies you install only in a project that browses:

pnpm add @browserbasehq/stagehand@3.6.0 playwright@1.59.1
pnpm exec playwright install chromium

If you declare a browser tool but the packages are missing, the agent fails at startup with one error naming what to install, instead of a "module not found" deep inside the first tool call.

The model-backed tools (browser_act, browser_extract, browser_observe, browser_login) reason about the page through the deployment's model provider. The runtime injects MODEL_PROVIDER_API_KEY for you. stackbone dev seeds whatever you exported in your shell or configured in the Studio Model provider screen, so your agent code never wires a key.

Attach the browser two ways

The two forms differ in how much of the browsing loop your main agent carries.

browserSubagent() attaches a ready-made browser specialist in one line. Your main agent hands the whole navigation loop to it and gets back only the final result. You gain two things:

  • A cleaner main transcript. The click-by-click back-and-forth stays in the subagent's own context instead of filling your main agent's history.
  • Containment against prompt injection. The subagent carries only the six browser tools, so text it reads off a web page can never reach your main agent's connectors, files, or database.

The subagent shares the parent's browser, so delegating does not open a second Chromium.

deep-agents/researcher/index.ts
import { defineDeepAgent, browserSubagent } from '@stackbone/sdk/deep';

export default defineDeepAgent({
  name: 'researcher',
  model: 'anthropic/claude-sonnet-4.5',
  subagents: [browserSubagent({ model: 'anthropic/claude-haiku-4.5' })],
});

Pass browserSubagent({ model }) to run the navigation loop on a cheaper or faster model than your main agent. Pass browserSubagent({ allowedDomains }) to fence in where it may browse.

Tell the parent agent to delegate in its own prompt, keyed researcher: "You research topics. Hand any web browsing to the browser subagent." The browser subagent brings its own instruction, so you never write one for it.

Put the tools on the agent directly

Spread browserTools() into the agent's own tools array when browsing is the agent's job, so there is no separate context to protect. This is also the form to use when you want to pause on an individual browser action for approval.

deep-agents/navigator/index.ts
import { defineDeepAgent, browserTools } from '@stackbone/sdk/deep';

export default defineDeepAgent({
  name: 'navigator',
  model: 'anthropic/claude-sonnet-4.5',
  tools: [...browserTools({ allowedDomains: ['example.com'] })],
  interruptOn: { browser_act: true }, // pause for a human decision before each action
});

Approval works through the subagent too. A tool gated with interruptOn inside the browser subagent still pauses the whole run and shows up in the approvals inbox, the same as a top-level pause. See Approvals for the decision flow.

Options

Both helpers take the same core options.

browserTools(options?)

Option Type Default What it does
model string anthropic/claude-haiku-4.5 The model the page-reading tools (browser_act, browser_extract, browser_observe) use, via OpenRouter. Trades cost for accuracy. browser_goto and browser_screenshot ignore it.
extractSchema Zod or JSON Schema none The output shape for browser_extract, so the tool returns typed data instead of raw HTML. Without it, browser_extract returns whatever the instruction implies.
allowedDomains string[] allow all The hosts the agent may browse. See Restrict where the agent can browse.
import { z } from 'zod';
import { browserTools } from '@stackbone/sdk/deep';

// browser_extract now returns typed data matching this shape.
const tools = browserTools({
  model: 'anthropic/claude-sonnet-4.5',
  extractSchema: z.object({ title: z.string(), price: z.number() }),
  allowedDomains: ['example.com'],
});

browserSubagent(options?)

Option Type Default What it does
model string or a LangChain model inherits the main agent The model the subagent's own reasoning loop uses. A bare id string routes through OpenRouter; a built model instance passes through as is.
allowedDomains string[] allow all Passed into the bundled browserTools({ allowedDomains }), so the subagent restricts hosts the same way as the direct-tools form.

Restrict where the agent can browse

By default an agent may open any http or https page, which is the right default for a local development tool. In production you want a fence. Set allowedDomains and the tools fail closed:

  • browser_goto refuses to open a host that is not on the list, and re-checks the page it landed on after any redirect. A redirect injected by a page cannot smuggle the agent onto an off-list host.
  • browser_act, browser_extract, browser_observe, and browser_screenshot refuse to act on, read, or capture a page whose current host is off the list.
  • The tools always refuse non-web schemes like javascript:, data:, and file:, whatever the list says.

A host matches by exact name or by a real subdomain. example.com allows example.com and app.example.com, but not notexample.com or example.com.evil.com.

Tighten the allowlist per install

An operator can narrow the allowlist without a code change. Declare an allowedDomains field in your config schema, and a non-empty value set in Studio wins over the list you passed in code. An empty or unset config value falls back to the code list, so a blank field never widens your fence.

Pre-check a URL yourself

checkDomainAllowed(url, allowlist) is the same rule the tools use, exported so you can validate a URL before you pass it on. It does no network calls and needs no browser:

import { checkDomainAllowed } from '@stackbone/sdk/deep';

const ok = checkDomainAllowed('https://app.example.com/login', ['example.com']);
// ok.allowed === true

const blocked = checkDomainAllowed('javascript:alert(1)', ['example.com']);
// blocked.allowed === false; blocked.reason explains why

One browser per session

The runtime keeps one browser per session. The first browser tool call in a run launches Chromium and caches it. Every later call in the same run reuses that same page, with its cookies and current tab intact. That reuse keeps the agent logged in across many turns and across a pause for human approval.

  • A chat conversation keeps its browser across its turns and across a pause, keyed by its durable session.
  • A workflow-triggered run gets its own browser, so two runs happening at once never share one Chromium's cookies.
  • A subagent reuses its parent's browser, so delegating to the browser subagent does not open a second one.

A pooled browser can still die mid-conversation: a remote session hits its server-side timeout, a local Chromium crashes, a debugging target closes. The runtime checks the cached browser before it hands it back, and drops it when a call fails on a closed session. The next tool call opens a fresh browser under the same key with the saved sign-in sessions injected again, so the conversation carries on already authenticated. The one call that hit the dead browser still returns { "ok": false, "error": "..." } rather than crashing the run.

Choose which browser the agent drives

An agent can drive one of three browsers. The operator picks which one in Studio, under Browser settings, on the Configuration tab. That screen is the way in: it holds the choice and the credentials for the whole installation, and it works on a deployed box nobody signs into over a shell.

Mode What it opens What the screen asks for
Local browser A plain Chromium on the box. No credentials, but many sites block it as a bot. Nothing
Managed remote browser A Browserbase session. Fingerprint-clean, residential proxies, solves captchas. Project identifier, API key
Attach over a debugging URL A browser that is already running somewhere and exposes a remote debugging endpoint. The debugging URL

Press Test connection before you save. It resolves the local browser binary, checks the API key against the project, or asks the debugging endpoint for its version, depending on the mode you picked. Test needs every field of that mode filled in, because it probes what you typed rather than what is stored.

A save touches only the fields of the mode you are saving. Switching to the local browser therefore leaves the managed provider's API key alone, and only the vendor's dashboard could issue that key again. Inside the mode you are saving, an empty field is a deletion: that is how you clear a stored value.

An explicit mode wins over any inference. If you pick the managed remote browser and one credential is missing, the tool fails with a message naming the missing field and the screen to fix it in. It does not fall back to a local Chromium that the site you were trying to reach will block. Leave the mode unset and the runtime infers one from whichever credentials it finds, in this order: debugging URL, managed remote browser, local.

The tools read this configuration each time they open a browser. A change on the screen applies to the next browser session, with no agent restart.

Override it from the shell in local development

On a deployed installation these four fields are stored secrets, and Browser settings is the only screen that writes them. The runtime also reads every one of them from the process environment, and the environment wins over the screen. That is the local development path: a variable exported in the shell that runs stackbone dev overrides the installation without touching the screen. The screen tells you when it detects one, so a saved change that appears to do nothing has a visible reason.

Variable Effect
BROWSER_MODE The mode: local, browserbase, or cdp. Unset means "infer from whichever credentials exist".
BROWSERBASE_API_KEY API key for the managed remote browser.
BROWSERBASE_PROJECT_ID Project identifier for the managed remote browser, taken from its dashboard URL.
STACKBONE_BROWSER_CDP_URL Attach to a browser that is already running, over the Chrome DevTools Protocol.
STACKBONE_BROWSERBASE_KEEP_ALIVE Set to 0 to opt out of keeping the remote session alive. Needed on a Browserbase free plan.
STACKBONE_BROWSER_EXECUTABLE_PATH Pin the Chromium or Chrome binary the local browser launches, when the detected one is wrong.
STACKBONE_BROWSER_HEADED=1 Open a visible window instead of running headless, so you can watch a stackbone dev browsing agent.

A BROWSER_MODE that is none of the three values is refused, not guessed. The tool fails naming the value it found and the three it accepts. Surrounding spaces and casing are forgiven, nothing else is. Without that check, a hand-typed remote in a compose file would put the installation back on a local Chromium with no warning.

The first three names, and any name starting with STACKBONE_, are reserved. Studio's plain Secrets screen refuses to create, overwrite, or delete them, the same way it protects the model provider credential. Browser settings is the only screen that writes them. The box stores them encrypted and never shows them back to you. If you created them by hand before they were reserved, they keep working: the box adopts them on upgrade.

Get past a captcha with the managed remote browser

A headless local Chromium is easy for a site to spot, so browser_login can stall on a sign-in page guarded by a captcha. The managed remote browser gets through: it runs a real, fingerprint-clean browser behind residential proxies and solves captchas for you.

Open Browser settings, pick the managed remote browser, paste the project identifier and the API key from the Browserbase dashboard, press Test connection, and save. The next browser session runs remote.

Only the browser moves. The page-reading tools still reason through your deployment's model provider. Saved sign-in sessions keep working too: the agent reuses a login it solved once, captcha included.

Watch the agent browse

When the agent runs on the managed remote browser, the runtime streams the live session into the chat while the agent navigates. The Studio playground and an embedded chat surface pick it up on their own and show the running browser, so you watch it click and type in real time. The panel appears when the browser opens mid-turn and disappears when the turn ends. The runtime stores nothing, so a dead session cannot linger.

There is nothing to wire in your agent. The local browser and the debugging-URL attach expose no hosted view, so the panel only shows up on the managed remote browser.

What's next

  • Agent examples: three complete agents you can copy as a starting point.
  • Stackbone Connect: the other way an agent reaches the outside world, through brokered connectors.
  • Approvals: gate a browser action behind a human decision.
  • Config: declare the allowedDomains field an operator tightens per install.
BUILT WITH ❤️ FROM CANADA AND SPAIN