Browser sessions
Claim a dedicated browser for your agent, drive it with batched browser actions in one round trip, or connect Playwright and Puppeteer over CDP.
A session is one whole browser process, yours alone, claimed from a warm pool and ready to drive. Use a session when your agent needs to click through an app, fill forms, or keep state across many steps. For a single page, a scrape with actions is cheaper.
Create a session
bashcurl -s https://beta-api.b3os.org/v1/vision/sessions \ -H "Authorization: Bearer $B3OS_API_KEY" \ -H "Content-Type: application/json" \ -d '{"viewport": {"width": 1440, "height": 900}, "timeout_s": 600}'
tsimport { B3OS } from "@b3os/sdk";const b3os = new B3OS();const session = await b3os.vision.createSession({ viewport: { width: 1440, height: 900 }, timeout_s: 600 });console.log(session.id, session.cdp_url);
pythonfrom b3os import B3OSb3os = B3OS()session = b3os.vision.create_session({"viewport": {"width": 1440, "height": 900}, "timeout_s": 600})print(session["id"], session["cdp_url"])
textOpen https://app.example.com/login in a browser, keep it open, and tell me what the page asks for.
The agent calls vision_act with url and keep_session: true. Without keep_session the tool closes the session it created when the call ends; with it, the result carries the session ID for the next call. vision_act is on the local MCP server only, and it takes no profile: to start signed in, create the session from code with profile and give the agent its ID.
The session is active until you close it or timeout_s passes (300 seconds by default, at most 1,800). It bills vision.browser_ms from claim to close. Other fields: profile to start signed in, persist_profile to save cookies back when it closes, locale, timezone, user_agent, block_ads and block_resources.
Drive it with batched actions
POST /v1/vision/sessions/{session_id}/act runs a list of browser actions in order on one page and returns every result in one round trip, so an agent pays one call for a whole sequence:
bashcurl -s "https://beta-api.b3os.org/v1/vision/sessions/$SESSION_ID/act" \ -H "Authorization: Bearer $B3OS_API_KEY" \ -H "Content-Type: application/json" \ -d '{"actions": [ {"kind": "navigate", "url": "https://app.example.com/login"}, {"kind": "type", "target": {"selector": "#email"}, "text": "agent@example.com"}, {"kind": "click", "target": {"role": "button", "name": "Continue"}}, {"kind": "wait", "for": {"selector": "[data-test=\"dashboard\"]"}}, {"kind": "markdown"} ]}'
tsconst result = await b3os.vision.act(session.id, { actions: [ { kind: "navigate", url: "https://app.example.com/login" }, { kind: "type", target: { selector: "#email" }, text: "agent@example.com" }, { kind: "click", target: { role: "button", name: "Continue" } }, { kind: "wait", for: { selector: '[data-test="dashboard"]' } }, { kind: "markdown" }, ],});console.log(result.ok, result.results.at(-1)?.markdown);
pythonresult = b3os.vision.act(session["id"], {"actions": [ {"kind": "navigate", "url": "https://app.example.com/login"}, {"kind": "type", "target": {"selector": "#email"}, "text": "agent@example.com"}, {"kind": "click", "target": {"role": "button", "name": "Continue"}}, {"kind": "wait", "for": {"selector": '[data-test="dashboard"]'}}, {"kind": "markdown"},]})print(result["ok"], result["results"][-1].get("markdown"))
textIn the open session, log in with agent@example.com, wait for the dashboard and read it back to me.
The agent calls vision_act with the session's session_id and the steps, which run as one batch.
Each result has its kind, status (ok, failed or skipped) and duration_ms, plus what the action produced: the URL and status code for navigate, the value for evaluate, the image for screenshot, the page for markdown. Execution stops at the first failed action unless continue_on_error is set; later actions are reported as skipped, and the failed one carries its error, such as target_not_found. A batch holds up to 100 actions and must finish within timeout_ms (30 seconds by default, 60 at most). Set target_id to act on a page other than the most recently active one; GET on the session lists its pages.
Browser actions
| Kind | Fields | Does |
|---|---|---|
navigate | url, wait_until (load, domcontentloaded, networkidle) | Load a URL |
click | target, button, count, modifiers | Click an element |
hover | target | Move the pointer over an element |
type | text, target, clear, interval_ms | Type into the focused or targeted field |
key | key (such as Enter), modifiers | Press a key |
scroll | to, by or target | Scroll to a point, by an amount, or until the target is in view |
select | target, value | Choose an option in a select |
wait | ms, or for one of selector, text, url, network_idle | Wait a fixed time or for a condition |
evaluate | expression | Run JavaScript in the page and return its JSON result |
screenshot | target, full_page, format, quality | Capture the page or an element |
markdown | only_main_content | Return the page as Markdown |
A target matches by CSS selector, visible text (exact for a whole match) or ARIA role plus accessible name, optionally within a scope; nth picks one of several matches. Actions that wait for a target take timeout_ms (10 seconds by default). Render scripts use the same kinds and fields, except evaluate, screenshot and markdown.
Connect over CDP
Every session has a cdp_url, a WebSocket that speaks the Chrome DevTools Protocol for that session only. Point Playwright or Puppeteer at it with your key in the Authorization header:
tsimport { chromium } from "playwright";const browser = await chromium.connectOverCDP(session.cdp_url, { headers: { Authorization: `Bearer ${process.env.B3OS_API_KEY}` },});const page = browser.contexts()[0].pages()[0] ?? (await browser.contexts()[0].newPage());await page.goto("https://example.com");console.log(await page.title());
tsimport puppeteer from "puppeteer-core";const browser = await puppeteer.connect({ browserWSEndpoint: session.cdp_url, headers: { Authorization: `Bearer ${process.env.B3OS_API_KEY}` },});const [page] = await browser.pages();await page.goto("https://example.com");console.log(await page.title());
The CDP proxy exposes only the session's own pages. It refuses SystemInfo.*, Tethering.*, Extensions.* and Cast.*, and every Browser.* method except the ones Playwright and Puppeteer need to drive a browser (Browser.getVersion, the permission methods, window bounds, Browser.setDownloadBehavior and Browser.cancelDownload). It also refuses other sessions' targets, host file access (DOM.setFileInputFiles and file drags), downloads to host paths (downloads land in the session's own directory, wiped with the browser), and navigation to anything but http:, https: and about:blank. A refused call gets a CDP error with code cdp_method_blocked. Creating a browser context with its own proxy (proxyServer, proxyBypassList or originsWithUniversalNetworkAccess, which Playwright's newContext({proxy}) sends) is refused with forbidden: all of the session's traffic goes through its own egress route. A client that falls more than a few megabytes behind the stream is disconnected.
From a browser page, where you cannot set headers, get a single-use stream ticket with POST /v1/vision/sessions/{session_id}/ticket and open the socket within 30 seconds with ?ticket=tkt_....
Close the session
bashcurl -s -X DELETE "https://beta-api.b3os.org/v1/vision/sessions/$SESSION_ID" \ -H "Authorization: Bearer $B3OS_API_KEY"
Closing destroys the browser and returns the session with its final usage.browser_ms. With persist_profile, cookies and storage are saved to the profile first. Closing a closed session returns it unchanged. Sessions that are closed or expired stay listed for 7 days; calling act on one fails with session_closed.