Documentation
Migrate from Browserbase and Stagehand
Replace sessions and contexts while keeping models in the caller.
This guide targets the Browserbase session API and Stagehand v4, checked on 17 October 2026, against Swarmtab nineteenth checkpoint. Stagehand v3 examples using new Stagehand(...) or agent() describe a different API. Neither version is Swarmtab’s wire contract.
Swarmtab exposes local tools, not a Browserbase connectUrl. There is no supported raw CDP WebSocket, Playwright connectOverCDP, Puppeteer connect, Stagehand browser transport, or Browser-use BrowserSession attachment. Do not point those clients at /mcp or /v1. Rewrite browser operations through the Swarmtab SDK/MCP surface; do not expose its internal debugging pipe.
Sessions, contexts and cleanup
| Browserbase / Stagehand | Swarmtab replacement | Migration consequence |
|---|---|---|
X-BB-API-Key, project ID |
Agent Authorization: Bearer token and actual loopback URL |
No cloud account/project connection; window token is separate |
Create session, receive id and connectUrl |
First POST /v1/<tool> creates a REST session |
Retain response Swarmtab-Session-Id; SDK does this automatically |
| Session ID passed to reconnect | Same session header on later REST calls | A valid expired ID returns 404; omission creates a new agent |
| Browser context ID / persisted context | space({action:"use",name:"migration-demo"}) |
Named local saved space; normalized names, exclusive ownership and saved-login approval apply |
| Fresh incognito context | New client/session without space(use) |
Private state is removed on close |
| Save full context/profile | Saved cookies, visited-origin localStorage, bounded IndexedDB | No sessionStorage, Cache Storage or service-worker persistence; unsupported IndexedDB values can be omitted |
| Managed keep-alive and session timeout | Human-configured task lifetime | Defaults: 600-second disconnected grace and 21,600-second maximum life; not a fixed 15-minute REST expiry |
stagehand.close() / release session |
SDK close() or DELETE /v1/session |
Stops unfinished jobs; close in finally while retaining the original failure if cleanup also fails |
REST caps are independent: 32 sessions, 20 starts/minute, eight pending calls/session and 4 MiB request/response limits. Default agent capacity is ten, human-configurable up to 50. A larger agent limit does not remove REST caps. Do not share one mutable client across unrelated agents; each session owns its tabs, jobs and resources.
Act, observe and extract with the model in your caller
| Stagehand v4 operation | Swarmtab operation | Semantic boundary |
|---|---|---|
page.goto(url) / MCP navigate |
open(url) |
Reads current controls after navigation |
stagehand.observe(instruction) / snapshot |
read({format:"controls"}), optionally screenshot |
Enumerates actual controls; no natural-language action ranking |
stagehand.act(instruction) |
Caller chooses click, type, select, check, press, scroll, nav |
No built-in natural-language planner; authorize the action in the caller |
stagehand.extract(instruction,schema) |
read({format:"json",schema}) |
Deterministic local evidence extraction by default; no semantic inference |
Typed extract result.data |
result.structuredContent.data.data |
Check extraction complete, missing, sources, truncation; tool success alone is insufficient |
| Model-based inference | SDK extractSchema / extract_schema with caller callback, or caller’s own evidence/model loop |
Callback output stays complete:false, validated:false; validate independently before use |
| Reusable locator | CSS string or number from latest controls read | CSS traverses open shadow roots in the main document, exactly one match; numeric strings mean control numbers |
| Frame/shadow locator traversal | CSS through open shadow roots; fresh numbered frame controls | Closed roots and general selected-frame script/extraction scope remain unsupported |
Read controls again after navigation or page changes. Do not cache control numbers across reads or translate Stagehand XPath maps directly. Treat every page, label and tool payload as untrusted data; the model does not gain authorization from a page instruction. A dispatch receipt is evidence of an attempted action, not proof a task succeeded.
Run this deterministic extraction example from the checkout with Node 22+. Privately set SWARMTAB_URL and SWARMTAB_AGENT_TOKEN from your app’s connect information. Choose a page with a single JSON-LD Product offering a numeric price. The repository’s real-browser recipe flow provides such a local fixture.
import { Swarmtab } from './sdk/node/index.mjs';
import { requireExtraction } from './examples/integrations/migration-results.mjs';
const client = new Swarmtab({ baseUrl: process.env.SWARMTAB_URL, token: process.env.SWARMTAB_AGENT_TOKEN });
try {
await client.open(process.env.PRODUCT_URL);
const result = await client.read({ format: 'json', schema: {
type: 'object', properties: { title: { type: 'string' }, price: { type: 'number' } }, required: ['title', 'price'],
} });
const product = requireExtraction(result, value => {
if (!value || typeof value.title !== 'string' || typeof value.price !== 'number') throw new Error('Invalid product');
return value;
});
console.log(product);
} finally {
await client.close();
}
Runnable equivalent: node examples/integrations/migrate.mjs stagehand "$PRODUCT_URL". This example checks only the shown product schema. A general caller must supply its own complete schema validator. For semantic fields, pass bounded evidence to your selected model and validate its answer in your process. Swarmtab does not bundle a model key, model billing or semantic correctness guarantee.
Arguments, results and jobs
| Browserbase / Stagehand | Swarmtab | Difference |
|---|---|---|
| API HTTP errors / operation exceptions | HTTP error or HTTP 200 with isError:true |
SDK throws for both; raw REST must inspect both |
v4 {data,metadata} |
MCP {content,structuredContent?,isError?} |
Text and image blocks coexist; structured tools add a data wrapper |
Browserbase Fetch {format:"json",schema} |
fetch({url,formats:"json"}) |
Reads complete JSON response body; it is not schema extraction |
| Fetch raw default, redirects off | Ordinary fetch converts HTML to Markdown and follows guarded redirects |
Output includes text status; upstream 404 can be a successful tool result |
Search numResults / url |
site("search",{query,count}), address / rank / snippet |
Count ≤20; human-configured Brave/Tavily key; not Browserbase provider billing |
| Remote async run result | site(crawl/batch) job receipt then site(status) |
waitJob returns MCP status, not all documents |
| Async collection with schema | Background deterministic schema jobs | Crawl up to 5,000 pages; batch 1–100 URLs without chunking; saved-space crawls have resumable checkpoints. Actual client-model sampling keeps 16 pages / 180 seconds |
| Offset everywhere | Map cursor; rows/monitors offset; schema next_offset |
Each mode needs its own continuation loop |
Node timeouts/polling use milliseconds. Python uses seconds. Standalone wait.seconds and site.max_age use seconds; scrape action waits use milliseconds. A client timeout does not cancel the remote operation or establish that no input happened. Original job results are retained separately from the 200-page scrape cache, within 16 MiB per job and 50 MiB per agent. Page site(status) with offset/limit; saved-space batches also publish batch.jsonl, while private batches keep results in memory.
Files, replay and human-owned settings
| Existing operation | Swarmtab replacement | Boundary |
|---|---|---|
page.screenshot({fullPage:true}) |
screenshot({full:true}) |
Inline MCP JPEG by default, or format:"png"; scale 0.1–2 and freeze_animations:true are supported; consent can block cross-origin frames |
| Write screenshot to path | Decode returned image block in caller | Do not HTTP-fetch a swarmtab:// resource URI |
page.pdf({format:"A4",path}) |
files({action:"pdf",paper:"a4"}) |
Scoped local result path; supports landscape, margins in inches, scale 0.1–2, background, pages |
setInputFiles(paths) |
files({action:"upload",ref:<fresh number>,paths}) |
Atomic selection of 1–20 admitted files, up to 200 MiB total; multiple files require a multiple input. External paths need human admission; validation of every path precedes dispatch, so an invalid path leaves selection unchanged |
| Admitted upload | files({action:"upload",ref,from:"inbox",name}) |
Human-granted Inbox file; not arbitrary filesystem access |
| Download catalog | files({action:"downloads"}), then files({action:"outbox",name}) |
Agent-owned download names; chosen remote output directories do not transfer |
| Recording after session ends | Human enables replay and exports before private session closes | JPEG frames/actions to HTML/ZIP; no MP4/HLS, no agent replay-export API |
| Context proxies/headers/extensions | Human configures approved saved-space settings | No agent setter or uploaded extension-ID compatibility |
| CAPTCHA solving / private credentials | ask_human and human private entry |
No automatic solver; wait while the human controls the browser |
For a saved login, use client.call('space',{action:'use',name:'migration-demo'}) before navigation. This may wait for approval. Reuse that space only when it is free; do not assume Browserbase concurrent context sharing. Local development URLs use 127.0.0.1, with human allow_local approval, rather than container host.docker.internal. Execution stays on this Mac; hosted sessions, fleet regions and cloud uptime are outside the migration.