Skip to content

Browser Automation ​

The Browser module wraps Playwright behind a managed surface: reusable browser profiles, ephemeral sessions that own a real Chromium tab, and a per-browser MCP endpoint so agents (and any MCP-aware client) can drive the same toolset.

It replaces the previous standalone "Browser Agent" feature. Console-managed agents now browse by attaching the Browser Use system tool to a project agent; everything else (external runtimes, custom orchestrators) talks to the browser session API or per-browser MCP directly.

Operators manage browsers under Operate → Browser.

Browsers list

The landing page lists browser profiles with their active/disabled state and live-session counts. Browsers, Sessions, Flows and Playground are peers in the left nav — each is its own screen, not a tab buried inside another.

Concepts ​

  • Browser profile — long-lived container with defaults for session config, the artifact bucket where screenshots/PDFs land, and a default model (used by extract/LLM-driven actions). Persisted in browsers.
  • Session — a live Playwright context. Created from a profile, carries its own status (starting | ready | closed | error), receives actions, and records every event (navigation, click, extract, screenshot) to browser_session_events. Auto-closes after idleTimeoutMs.
  • Action — a single operation against a session: goto, click, hover, type, press, select, check, upload, drag, scroll, wait, tab, back/forward/reload. The full schema lives in browserActionSchema.
  • Target — how an action names the element it acts on. See Addressing elements; this is the difference between an automation that works once and one that keeps working.
  • Flow — a recorded, replayable step list. See Flows.
  • Extract — a target + mode (text | html | attr | value), optionally over every match.
  • Artifacts — screenshots and PDFs are stored in the configured file bucket; the response carries the bucket key.

Addressing elements ​

An action can name its element two ways, and only one of them survives being saved.

browser_snapshot returns the page's accessibility tree with [ref=e4] markers. A ref is the cheapest, least ambiguous handle for the turn you are in — and it is valid only until the next snapshot, because the browser renumbers them every time. Stored in a flow, a ref looks like a working target and then spends the step's entire timeout resolving to nothing.

Everything else is durable: it describes the element the way a person would, so it still resolves after a re-render and usually after a deploy.

FieldUse when
testIdthe app sets data-testid — the most stable target there is
role + namealmost always: { "role": "button", "name": "Sign in" }
labela form field with a <label>
placeholderan input with placeholder text and no label
texta link or element identified by its visible text
selectorlast resort — CSS encodes markup nobody promised to keep
nthdisambiguates when the chosen strategy matches several elements
frameCSS selector of an iframe to look inside

Every action result carries resolvedTarget: the durable description of whatever the action actually hit. That is what you save.

jsonc
// Request — a live agent uses the ref it just saw
{ "type": "click", "ref": "e12" }

// Response — the durable form, safe to keep
{ "ok": true, "targetStrategy": "ref",
  "resolvedTarget": { "role": "button", "name": "Sign in" } }

A stale ref does not stall: when a durable target is supplied alongside it, the ref is probed briefly and then abandoned in favour of the durable one.

Flows ​

Driving a browser with a model is discovery — it reads the page, guesses, backtracks, and bills tokens for every step. Replaying a flow is execution: no model, no guessing, the same steps every time.

A flow is an ordered list of steps with durable targets, declared inputs, and per-step retry policy. Record one from a session you already drove — by hand in the live preview, or with an agent:

bash
curl -X POST /api/client/v1/browser/flows/record \
  -d '{ "sessionId": "…", "name": "Submit expense", "status": "active" }'

Recording substitutes durable targets for refs, and turns every typed value into a declared input rather than a literal — the recorder cannot tell a search term from a password, and only one of those mistakes is recoverable. Steps reference them as {{input.name}}.

Replay it with different values, as often as you like:

bash
curl -X POST /api/client/v1/browser/flows/<key>/run \
  -d '{ "inputs": { "reference": "EXP-2002", "amount": "999" } }'

The response is the run record: per-step status and attempt count, whatever captureAs collected, and — on a failure — the index of the step that broke plus a screenshot of the page it gave up on.

Step policy:

FieldEffect
policy.retriesattempts beyond the first; the delay doubles
policy.timeoutMsper-step bound, overriding the session default
policy.optionala failing step is recorded and skipped instead of aborting
whenskip the step unless the expression is truthy
captureAsstore the step's output for later steps and the run's outputs

A run aborts at the first non-optional failure: a half-finished form is usually worse than an untouched one.

Agents reach flows through browser_list_flows and browser_run_flow — check for an existing flow before working a task out step by step.

Flows live at Operate → Browser → Flows.

A flow's steps and run history

Every step shows the durable target it was recorded with — never a ref — and the run history underneath is a per-step ledger: status, attempt count and duration, so a failure names the step that broke rather than just the flow.

Signed-in profiles ​

An unattended run should not push credentials through a login form every time. A browser profile can carry a Playwright storageState — cookies plus origin storage — so every session it opens starts already authenticated:

bash
curl -X PUT /api/client/v1/browser/browsers/<idOrKey>/profile \
  -d '{ "storageState": <contents of profile.json> }'

Get that file either from Playwright (context.storageState()) or from a session you signed into by hand — the session list has a save as profile action, and GET …/sessions/<key>/profile returns the same JSON.

The payload is encrypted at rest and is never readable back; the API returns only a summary (cookie count, origins, earliest expiry) so the dashboard can warn you before a profile goes stale.

Browser overview: activity, flows, profile and session defaults

The overview leads with what the browser is DOING — live sessions, a 7-day sparkline, recent runs, the flows recorded against it — rather than a static list of its own fields. The signed-in profile and default session settings sit below it, still one scroll away.

Quick start ​

bash
# 1. Create a profile (one-time)
curl -X POST /api/browser/browsers \
  -d '{
    "name": "research-browser",
    "defaultSessionConfig": { "headless": true, "viewport": { "width": 1440, "height": 900 }, "idleTimeoutMs": 120000 }
  }'

# 2. Open a session
curl -X POST /api/browser/sessions \
  -d '{ "browserId": "brw_…", "name": "akbank-research" }'

# 3. Drive it
curl -X POST /api/browser/sessions/<sessionKey>/actions \
  -d '{ "type": "goto", "url": "https://www.akbank.com", "waitUntil": "networkidle" }'

curl -X POST /api/browser/sessions/<sessionKey>/extract \
  -d '{ "selector": "h1", "mode": "text", "multiple": true }'

# 4. Capture artifacts
curl -X POST /api/browser/sessions/<sessionKey>/screenshot \
  -d '{ "fullPage": true }'

curl -X DELETE /api/browser/sessions/<sessionKey>

The live screenshot endpoint (GET …/screenshot/live) returns an inline PNG/JPEG with cache-control: no-store — use it to drive a preview pane in the dashboard.

Per-browser MCP ​

Every profile exposes its own MCP server at /api/client/v1/browser/:browserKey/mcp/*. The toolset mirrors the action API but follows the Model Context Protocol:

browser_navigate · browser_history · browser_click · browser_hover ·
browser_type · browser_press · browser_select · browser_check ·
browser_upload · browser_scroll · browser_wait · browser_tabs ·
browser_snapshot · browser_find · browser_extract · browser_diagnostics ·
browser_screenshot · browser_pdf · browser_list_flows · browser_run_flow ·
browser_close

The list is derived from the same tool definitions the Browser Use system tool binds, so the two surfaces cannot drift apart.

Open the SSE stream first:

http
GET /api/client/v1/browser/:browserKey/mcp/sse

The response carries an X-Mcp-Session-Id header and an endpoint SSE event with the message URL. Subsequent calls go to:

http
POST /api/client/v1/browser/:browserKey/mcp/message?sessionId=<id>

…with the standard JSON-RPC payload (initialize, tools/list, tools/call, etc.). This is what the Browser Use system tool uses under the hood, so any MCP-compatible agent runtime can connect the same way.

Sessions in the dashboard ​

/dashboard/browser/[browserId] shows the profile's activity, its flows, and its signed-in profile and session defaults. /dashboard/browser/[browserId]/sessions lists every session for that profile.

Sessions in the left nav (/dashboard/browser/sessions) is the project-wide view: every session across every browser, with filters for browser, status and a time range (last hour / 24h / 7 days / 30 days / a custom range) — the question this page answers is "what ran last night and what is still open", which needs a time axis a single-browser list doesn't.

Every session in the project, with time-range and status filters

The Started by column names where a session came from — an agent, a recorded flow, MCP, or a person driving it by hand — which is the first thing worth knowing when a session misbehaves.

Playground ​

Playground (/dashboard/browser/playground) drives a session interactively: controls on the left, the live page on the right, so acting on an element never means losing sight of it.

The playground: action composer on the left, live preview on the right

The right pane's Elements tab lists the page's interactive elements — clicking one fills the action composer with its durable target (the same role + name a recorded flow step stores), so discovering a step and recording it use the same gesture. Console surfaces the page's own console messages and failed requests, for when an action succeeded but the page didn't do what you expected.

Once a session has run a few steps, Record as flow freezes them into a flow — this is the fastest path from "I drove it once by hand" to "it runs every night".

Distributed execution ​

Browser is one of the entity types the Cluster layer can pin to a specific node. The browserConsumer registers a queue handler; when a profile is assigned to a node, all of its sessions and actions route through that node's queue. This matters when only some nodes have a real browser binary installed.

Concurrency ​

Each profile has a concurrency ceiling defined by its defaultSessionConfig and the worker pool in browserManager.ts. When the ceiling is hit, new createSession calls are queued — the session starts as soon as a slot frees.

See the Browser API reference for the full endpoint list.

Step by step: automate a browser task ​

Console runs Chromium through Playwright on the server and puts a small, auditable API in front of it. You create a browser profile once, open a session against it, and drive that session with discrete actions — navigate, click, type, extract, screenshot, PDF. Every operation is logged against the session, and screenshots and PDFs land in a Document Store bucket.

This page takes you from an empty tenant to a completed run, first through the dashboard and then over the API, and finishes by handing the same browser to an agent over MCP. Once you have driven a task once, Browser Flows let you record it and replay it deterministically — no model, no re-discovery — which is the piece that turns this from "an API for browsing" into an RPA substitute; this page covers the direct, discovery-style automation underneath it.

Browsers are a community feature. Only the multi-node routing note at the end depends on the Enterprise Cluster module.

What you will end up with ​

  • A browser profile visible under Operate → Browsers, with a stable key and id.
  • A live session that you can watch in the dashboard while it works.
  • A page snapshot you can feed to a model, and a screenshot plus a PDF stored as files with download URLs.
  • An MCP endpoint for that profile that any MCP-aware client — including a Console agent — can connect to.

What the service does ​

Every call is synchronous. The HTTP request blocks until Playwright finishes the operation or the timeout fires; there is no job id and nothing to poll. Two background jobs exist, but neither is in your request path: an idle reaper that closes stale sessions, and a boot-time reconciliation that marks sessions the runtime lost.

OperationEndpoint, under /api/client/v1/browserResult
Run an actionPOST /sessions/:sessionKey/actionsOne of goto, click, hover, type, press, select, check, upload, drag, scroll, wait, tab, back/forward/reload
Read the DOMPOST /sessions/:sessionKey/extracttext, html, attr or value, one element or all matches
Snapshot the pageGET /sessions/:sessionKey/snapshotThe accessibility tree as YAML, carrying [ref=…] markers
Find visible textGET /sessions/:sessionKey/find?text=…A durable target per hit — cheaper than a full snapshot, and safe to save into a flow step
DiagnosticsGET /sessions/:sessionKey/diagnosticsConsole messages, failed requests, and the last dialog the session saw
Live screenshotGET /sessions/:sessionKey/screenshot/liveRaw PNG/JPEG bytes, cache-control: no-store, not stored
Persist a screenshotPOST /sessions/:sessionKey/screenshotUploads to the artifact bucket, returns a download URL
Export a PDFPOST /sessions/:sessionKey/pdfSame, as application/pdf
Export as a profileGET /sessions/:sessionKey/profileThis session's storageState — hand it to PUT /browsers/:idOrKey/profile so future sessions start signed in
Close a sessionDELETE /sessions/:sessionKeyCloses the Chromium context
Delete the recordDELETE /sessions/by-id/:sessionIdCloses it and removes the row
Read the audit trailGET /sessions/:sessionId/eventsEvery operation the session performed

Note the split: the driving endpoints (actions, extract, snapshot, find, diagnostics, screenshot, PDF, profile export, close) take the sessionKey (bs_…), while reading a session, listing its events and deleting its record take the id. The full endpoint list — including Browser Flows for recording and replaying a task without a model — is in the Browser API reference.

What it does not do ​

  • No arbitrary JavaScript in the page — there is no general evaluate endpoint. The only script the runtime executes itself is window.scrollBy for a coordinate scroll.
  • No raw mouse coordinates and no video recording.
  • No interactive remote desktop. The dashboard's live preview polls the live-screenshot endpoint every few seconds; the Playground is the closest thing to hands-on-glass, and it is still built on that same poll.
  • No model in the loop for a direct action. extract reads the DOM literally; the profile's Default Model Key is stored and displayed but no browser runtime path reads it for a plain action. A model enters the picture when an agent drives the tools (see Give an agent the browser) — or, once you have driven a task once, a Browser Flow replays it with no model at all.

A few things that used to belong on this list no longer do: select, check, upload and drag actions exist now, a session can carry proxy, extraHTTPHeaders, httpCredentials, timezoneId and acceptDownloads in its config, a browser profile can carry a signed-in storageState (see Signed-in profiles), and a session can hold more than one tab via the tab action. dialogPolicy is accepted and stored on a session's config but nothing in the runtime applies it yet — a native alert/confirm/prompt still gets Playwright's default handling.

Before you start ​

You needWhyWhere
Playwright with Chromium installed on the serverThe runtime imports playwright lazily and fails the first session with an explicit install message if it is missingnpm install playwright then npx playwright install chromium
The right project selectedProfiles, sessions and buckets are project-scoped, and a token is bound to the project that was active when it was mintedHeader project pill
A file bucket for artefactsScreenshots and PDFs are uploaded as files; the bucket must already existDocument Store — see Files
An API token, for the API half of this pageAuthorization: Bearer cpeer_…; the owner needs Browsers read for the GETs and write for everything elseSettings → API Tokens — see Authentication

If you skip the bucket, everything works until the first POST …/screenshot, which fails with File bucket not found. The default bucket key is browser-artifacts; either create a bucket with that key or set one explicitly on the profile.

Create a browser profile ​

  1. Open Operate → Browsers (/dashboard/browser) — its own entry in the left nav, a peer of Sessions, Flows and Playground, not a tab.
  2. Select Create browser. This opens a full-page form, in line with the rest of Console.
  3. Fill in its sections:
    • Identity — Name, required, 2 to 120 characters (the profile key is slugified from it and uniquified with -2, -3… if it collides), an optional Description, and an optional Default Model Key, stored and shown on the profile though nothing in the browser runtime reads it for a plain action today.
    • Browser window — viewport size, user agent, locale, timezone.
    • Timeouts — idle timeout, max lifetime, action and navigation timeouts.
    • Network and safety — the artifact bucket that receives screenshots and PDFs, and the allow/block host lists.
  4. Select Create.

Creating a browser: the Identity section of the full-page form

The row now shows the profile's Key, Default model, session counters and Status. Open it to reach the detail page, which has Overview, Sessions and Usage tabs, plus Edit and a Get MCP URL action.

Unlike the old modal, every defaultSessionConfig field the API accepts has a spot in this form — see Defaults and limits for the full field list and their ranges.

Run a session from the dashboard ​

Two places start a session, depending on what you're doing:

  1. On the profile's Overview tab, select New session for a session scoped to that one profile — the same full-page form covers name, initial URL, artifact bucket override and allow/block hosts.
  2. Playground (/dashboard/browser/playground), a top-level page in the left nav, drives any browser interactively: pick (or start) a session, and the action composer sits on the left with the live page on the right, so acting on an element never means losing sight of it.

The playground: action composer on the left, live preview on the right

The right pane's Elements tab lists the page's interactive elements — clicking one fills the action composer with its durable target, the same role+name a recorded flow step would store — and its Console tab surfaces the page's own console messages and failed requests. Once you've run a few steps, Record as flow freezes them into a Browser Flow directly from here.

The profile's own Sessions tab lists every session scoped to it; the left nav's project-wide Sessions page (/dashboard/browser/sessions) lists every session across every browser, with filters for browser, status and a time range — useful for "what ran last night and what is still open."

Session status is one of pending, running, idle, closed, expired or errored. expired means the reaper closed it on idle timeout or maximum lifetime, or that the runtime restarted underneath it.

The same thing over the API ​

Base URL: https://<host>/api/client/v1. Every request carries Authorization: Bearer cpeer_….

bash
export CONSOLE_HOST="https://console.example.com"
export CPEER_TOKEN="cpeer_..."
alias capi='curl -sS -H "Authorization: Bearer $CPEER_TOKEN" -H "Content-Type: application/json"'

1. Create the profile. Here the full session config is set up front, which the dashboard modal cannot do.

bash
capi -X POST "$CONSOLE_HOST/api/client/v1/browser/browsers" -d '{
  "name": "research-browser",
  "description": "Read-only research runs",
  "artifactBucketKey": "browser-artifacts",
  "defaultSessionConfig": {
    "viewport": { "width": 1440, "height": 900 },
    "locale": "en-GB",
    "idleTimeoutMs": 120000,
    "maxLifetimeMs": 900000,
    "navigationTimeoutMs": 45000,
    "access": { "allowList": ["example.com", "*.wikipedia.org"] }
  }
}'

201 with { "browser": { "id": "…", "key": "research-browser", … } }. Keep both: sessions need the id, the MCP endpoint uses the key.

2. Open a session.

bash
capi -X POST "$CONSOLE_HOST/api/client/v1/browser/sessions" -d '{
  "browserId": "6650f1c0a1b2c3d4e5f60718",
  "name": "example-run"
}'

201 with { "session": { "id": "…", "sessionKey": "bs_9f3c…", "status": "idle", … } }. browserId must be the profile's id; passing the key returns 404 Browser not found: research-browser.

3. Navigate.

bash
capi -X POST "$CONSOLE_HOST/api/client/v1/browser/sessions/bs_9f3c…/actions" -d '{
  "type": "goto",
  "url": "https://example.com",
  "waitUntil": "networkidle"
}'

The response is 200 with a result envelope:

json
{
  "result": {
    "ok": true,
    "url": "https://example.com/",
    "pageTitle": "Example Domain",
    "ariaSnapshot": "- heading \"Example Domain\" [level=1] [ref=e2]\n- link \"More information...\" [ref=e5]\n"
  }
}

Every successful action returns a fresh ariaSnapshot, so a click-then-read loop needs no extra round trip. Check result.ok, not the status code — a failed action is still 200, with the reason in result.errorMessage.

4. Address elements. The [ref=…] markers in the snapshot are the preferred way to target elements; a CSS selector is the alternative, and click, hover, type and press require one of the two.

bash
capi -X POST "$CONSOLE_HOST/api/client/v1/browser/sessions/bs_9f3c…/actions" -d '{
  "type": "click", "ref": "e5", "selector": "a[href*=iana]"
}'

Refs are bound to the snapshot that produced them. Supplying both ref and selector is the robust form: the runtime probes the ref for two seconds and falls back to the selector if the page has moved on.

5. Read the page.

bash
capi -X POST "$CONSOLE_HOST/api/client/v1/browser/sessions/bs_9f3c…/extract" -d '{
  "selector": "h1, p", "mode": "text", "multiple": true
}'

200 with { "result": { "ok": true, "values": ["…", "…"] } }. "mode": "text" (the default) reads innerText, "mode": "html" reads innerHTML, and "mode": "attr" needs "attribute": "href" — attribute is mandatory in that mode.

For the whole accessibility tree instead of one element:

bash
capi "$CONSOLE_HOST/api/client/v1/browser/sessions/bs_9f3c…/snapshot"

200 with { "ariaSnapshot": "…", "url": "…" }.

6. Capture artefacts.

bash
capi -X POST "$CONSOLE_HOST/api/client/v1/browser/sessions/bs_9f3c…/screenshot" \
  -d '{ "fullPage": true, "type": "png" }'

capi -X POST "$CONSOLE_HOST/api/client/v1/browser/sessions/bs_9f3c…/pdf" \
  -d '{ "format": "A4", "printBackground": true }'

Both return 201:

json
{
  "artifact": {
    "bucketKey": "browser-artifacts",
    "fileId": "…",
    "objectKey": "bs_9f3c…-1723731600000-4c8a1b2e.png",
    "contentType": "image/png",
    "url": "/api/client/v1/files/buckets/browser-artifacts/objects/bs_9f3c…-1723731600000-4c8a1b2e.png/download"
  },
  "eventId": "…"
}

objectKey is generated by the Document Store — it appends its own timestamp and a random suffix, so read it from the response rather than constructing it. artifact.url is a path on the same host; fetch it with the same bearer token. The bucket is resolved in this order: the session's artifactBucketKey, then the profile's, then the server default (BROWSER_DEFAULT_ARTIFACT_BUCKET, browser-artifacts). The screenshot pointer is also written to the session as lastScreenshot.

For a preview that you do not want to keep, GET …/screenshot/live returns the bytes directly and stores nothing.

7. Close the session.

bash
capi -X DELETE "$CONSOLE_HOST/api/client/v1/browser/sessions/bs_9f3c…"

200 with { "closed": true }. false means the runtime no longer held that session — the reaper or a restart had already taken it. Closing frees the tenant's concurrency slot, so do it explicitly rather than waiting for the idle timeout.

8. Read what happened.

bash
capi "$CONSOLE_HOST/api/client/v1/browser/sessions/<sessionId>/events?limit=50"

Note this one takes the session id, not the sessionKey.

With the SDK ​

@cognipeer/console-sdk (1.6.0) wraps the same endpoints:

ts
import { ConsoleClient } from '@cognipeer/console-sdk';

const client = new ConsoleClient({
  apiKey: process.env.COGNIPEER_API_KEY!,
  // Host root — a trailing /api/client/v1 is stripped if you include it.
  baseURL: process.env.COGNIPEER_BASE_URL!,
});

const browser = await client.browsers.get('research-browser');
const session = await client.browserSessions.create({ browserId: browser.id, name: 'example-run' });

await client.browserSessions.action(session.sessionKey, { type: 'goto', url: 'https://example.com' });
const snap = await client.browserSessions.snapshot(session.sessionKey);
const shot = await client.browserSessions.screenshot(session.sessionKey, { fullPage: true });

await client.browserSessions.close(session.sessionKey);

client.browsers.get() accepts an id or a key; browserSessions.create() still needs the id.

Give an agent the browser ​

Two routes lead to the same 21 tools.

In Console. Open the agent's Playground tab; the Configuration panel on the left has a Tools section. Select Add Tools (Edit Tools once the agent already has some). The Select Tools dialog lists Browser Use as a System source with a Browser picker: "Selecting a browser adds the Browser Use system tool to this agent." Choose the profile and select Save Configuration. At run time the agent opens its own session against that profile and closes it when the run ends. The agent workflow itself is covered in Build and publish an agent.

Over MCP. Every profile exposes its own MCP server. Use Get MCP URL on the profile — from the header menu or the Overview card — to copy both URLs, or build them yourself:

http
GET  /api/client/v1/browser/:browserKey/mcp/sse
POST /api/client/v1/browser/:browserKey/mcp/message?sessionId=<sessionId>

Authenticate both with Authorization: Bearer cpeer_…. The SSE response carries an X-Mcp-Session-Id header and an endpoint event containing the message URL with the session id already filled in. The message endpoint speaks JSON-RPC and handles initialize, notifications/initialized, ping, tools/list and tools/call.

Tools exposed, grouped by what they do:

GroupTools
Navigatebrowser_navigate, browser_history (back / forward / reload)
Act on an elementbrowser_click, browser_hover, browser_type, browser_press, browser_select, browser_check, browser_upload
Page and tabsbrowser_scroll, browser_wait, browser_tabs
Read the pagebrowser_snapshot (accessibility tree with [ref=…]), browser_find (durable target per text hit), browser_extract
Observebrowser_diagnostics (console, failed requests, last dialog)
Artifactsbrowser_screenshot, browser_pdf
Flowsbrowser_list_flows, browser_run_flow — replay a recorded flow with no model in the loop
Lifecyclebrowser_close

An action tool targets an element the same way the HTTP API does: a ref from the last browser_snapshot, or a durable role/name/testId/label/placeholder/text/selector. See Addressing elements — this is also what a flow records instead of the ref, so browser_run_flow keeps working across sessions where a saved ref wouldn't.

Lifecycle details that matter when you wire this up:

  • The first tools/call on an MCP session creates a browser session behind it; that one session serves the whole MCP session. initialize and tools/list work without a sessionId, but tools/call without one returns Missing sessionId; open SSE first.
  • Dropping the SSE stream closes the backing browser session.
  • A disabled profile returns -32002 Browser is disabled on the message endpoint and 403 on the SSE endpoint.
  • Tool failures come back as a normal JSON-RPC result with isError: true and the message as text, not as a JSON-RPC error.

Defaults and limits ​

Session configuration, merged as profile defaultSessionConfig then the session's own config:

FieldDefaultAccepted range
headlesstrueboolean — accepted and stored, but the launch mode comes from BROWSER_HEADLESS (see the gotcha below)
viewport1280 × 800width 320–8192, height 240–8192
userAgentChromium default≤ 512 characters
localeChromium default≤ 64 characters
timezoneIdChromium default≤ 64 characters
idleTimeoutMs300000 (5 minutes)1000 – 86400000
maxLifetimeMs1800000 (30 minutes)1000 – 604800000
actionTimeoutMs150001 – 120000
navigationTimeoutMs300001 – 300000
access.allowList / access.blockListnoneup to 100 entries each, ≤ 255 characters per entry
proxy.server / .username / .password / .bypassnoneserver + optional credentials, bypass ≤ 1000 characters
extraHTTPHeadersnoneheader map, keys ≤ 128 / values ≤ 2000 characters
httpCredentials.username / .passwordnoneHTTP basic auth for the context
acceptDownloadsbooleanaccepted and stored
ignoreHTTPSErrorsbooleanaccepted and stored
dialogPolicyaccept | dismissaccepted and stored, not yet applied — a native dialog still gets Playwright's own default handling
storageState—session-only: replay a storageState export for one session without attaching it to the profile; the profile-level signed-in profile is set separately, see Signed-in profiles

Per-action limits: type.text ≤ 10000 characters and type.delay 0–5000 ms; wait.ms 1–60000; scroll.x / scroll.y ±100000; screenshot quality 1–100 (JPEG only, default 80); PDF format one of A4, Letter, Legal, A3, A5 with printBackground defaulting to true.

Server-level settings:

VariableDefaultEffect
BROWSER_HEADLESStrueChromium launch mode
BROWSER_VIEWPORT_WIDTH / _HEIGHT1280 / 800Fallback viewport
BROWSER_DEFAULT_IDLE_TIMEOUT_MS300000Reaper idle window
BROWSER_DEFAULT_MAX_LIFETIME_MS1800000Hard session lifetime
BROWSER_DEFAULT_ACTION_TIMEOUT_MS15000Default per-action timeout
BROWSER_DEFAULT_NAVIGATION_TIMEOUT_MS30000Default goto timeout
BROWSER_REAPER_INTERVAL_MS30000Reaper sweep interval
BROWSER_DEFAULT_MAX_CONCURRENT10Live sessions per tenant
BROWSER_DEFAULT_ARTIFACT_BUCKETbrowser-artifactsFallback artefact bucket
BROWSER_BLOCK_PRIVATE_NETWORKtrueEgress guard, described below
BROWSER_CONCURRENCY_PROVIDERmemoryConcurrency limiter; memory is the only implementation

When the tenant is at its concurrency ceiling, POST /sessions queues for 60 seconds and then fails with Browser concurrency wait timed out after 60000ms.

What gets recorded ​

Each operation appends an event to the session with a monotonic sequence, a type (create, goto, click, hover, type, press, wait, scroll, extract, snapshot, screenshot, pdf, close), a status of success or error, a duration, the selector or ref, and the artefact reference where there is one. A failure keeps the operation's own type and records the reason in errorMessage. Two redactions are applied before anything is written:

  • URLs are stored with the query string and the fragment stripped, so tokens in links do not reach the log.
  • Text typed with the type action is replaced by [redacted:N chars]. The event log tells you that 24 characters went into the password field, never which ones.

Usage attribution emits one event per session under the service browser: a success event at close carrying the session duration as latency, or — if the session never opened — a single error event instead. Neither carries token counts or a cost, so browser sessions appear in usage surfaces as counted activity rather than money.

Two maintenance jobs are visible on Automations (/dashboard/automations) under the Browser domain, both with Run now:

  • Browser Session Reaper — closes idle or over-lifetime sessions inside the local runtime. Supports Pause and Resume.
  • Browser Session Reconciliation — after a restart, marks sessions the database still believes are active as expired with Browser runtime restarted before the session completed. It cannot be paused.

Gotchas ​

A failed action is still HTTP 200. POST /actions and POST /extract return { "result": { "ok": false, "errorMessage": "…" } } for anything Playwright refuses — a missing selector, a timeout, a blocked host. Branch on result.ok.

Egress denials look like action failures, not 403s. With BROWSER_BLOCK_PRIVATE_NETWORK on (the default), the runtime refuses hosts that resolve to loopback, private, link-local, CGNAT or multicast space, plus localhost, *.localhost, localhost.localdomain, *.local and *.internal. A host whose DNS lookup fails is also treated as private — the guard fails closed. Results are cached for five minutes. The denial arrives as result.ok: false with the reason in errorMessage. This is also why a session cannot reach a service on localhost or on a private address — including Console itself, when Console is reached that way.

Allow and block lists are evaluated per request, not just per navigation. They are applied to every resource the page loads. Order is: block list first (a match denies), then the allow list (if non-empty, a non-match denies), then the private-network guard. A pattern matches exactly, or as a suffix — example.com also matches www.example.com, while *.example.com matches subdomains only and not the apex. * allows everything.

Sessions live in one process's memory. They are not shared between nodes and do not survive a restart; a restarted runtime marks them expired. In an Enterprise Cluster deployment only POST /actions is routed to the node that owns the profile — extract, snapshot, screenshot, PDF and close execute on whichever node receives the request, so pin your traffic or keep the whole run on one node. Assigning a profile to a different node affects future sessions only.

Ids and keys are not interchangeable. browserId on session creation must be the profile's id; the MCP path accepts either the id or the key; driving endpoints take the sessionKey; reading a session, its events and deleting its record take the session id.

The bucket must exist before the first capture. A missing bucket surfaces as 404 File bucket not found. from the screenshot and PDF endpoints only — session creation and every action succeed without one.

PDF export requires headless mode. Chromium only renders PDFs headless, and the launch mode comes from BROWSER_HEADLESS (default true) — it is a server-wide setting. A headless value on a profile's defaultSessionConfig or on a session's config is accepted and stored, but the runtime never applies it, so you cannot turn headless off for one session.

Deleting a profile with sessions returns 409. The check counts session records, whatever their status, so closing them is not enough — delete them with DELETE /sessions/by-id/:sessionId first. The message is Cannot delete browser with existing sessions. Delete or archive sessions first.

The snapshot endpoint reports every failure as 500. Unlike the other routes it does not map errors to status codes, so a closed session there returns 500 with the message in error rather than 404.

Request bodies are strict. Every browser payload rejects unknown fields with 400, and the error message names only the first failing path — fix them one at a time.

Playwright is a runtime dependency, not a build one. If Chromium is missing, the first session fails with Playwright is not installed in this environment. and nothing before that point gives you a warning.

Studio · Pulse · Console · Agent SDK and more — the Cognipeer documentation hub