Skip to main content

Browser Automation

Wunderland agents can interact with web pages through the browser automation module. The system is built on top of Playwright and provides three layers: an HTTP client (BrowserClient), a CDP session manager (BrowserSession), and a high-level interactions API (BrowserInteractions).

Architecture​

BrowserClient (HTTP)          BrowserSession (CDP/Playwright)
| |
| /start, /stop, /tabs, | connect(), getPage(),
| /snapshot, /tabs/open | ensurePageState()
| |
+------- used by agents -----------+---- BrowserInteractions
click(), type(), goto(),
screenshot(), wait(), ...
  • BrowserClient -- HTTP client that talks to a browser automation service. Use this when the browser is running as a separate service (e.g., via the OpenClaw browser server).
  • BrowserSession -- Direct Playwright CDP connection. Use this when you need to control Chrome locally via playwright-core.
  • BrowserInteractions -- High-level wrapper around BrowserSession that provides agent-friendly methods for clicking, typing, form-filling, and more.

BrowserClient Setup​

BrowserClient communicates with a browser service over HTTP. It does not require playwright-core as a dependency.

import { BrowserClient } from 'wunderland/advanced/browser';

const client = new BrowserClient({
baseUrl: 'http://localhost:9222',
defaultTimeoutMs: 10000,
});

Configuration​

OptionTypeDefaultDescription
baseUrlstringhttp://localhost:9222Base URL for the browser service
defaultTimeoutMsnumber10000Default timeout for requests in ms

Browser Lifecycle​

// Check browser status
const status = await client.status();
console.log(status.running, status.cdpReady);

// Start the browser
await client.start();

// Stop the browser
await client.stop();

Profile Management​

Profiles allow multiple isolated browser instances with separate data directories.

// List all profiles
const profiles = await client.profiles();

// Create a new profile
const result = await client.createProfile({
name: 'agent-cipher',
color: '#00ffcc',
driver: 'openclaw',
});

// Delete a profile
await client.deleteProfile('agent-cipher');

Tab Management​

// Open a new tab
const tab = await client.openTab('https://example.com');
console.log(tab.targetId, tab.title, tab.url);

// List all tabs
const tabs = await client.tabs();

// Focus a tab
await client.focusTab(tab.targetId);

// Close a tab
await client.closeTab(tab.targetId);

Snapshots​

Snapshots capture the current page state in a format suitable for agent consumption. Two formats are available:

  • aria -- Returns an array of accessibility tree nodes (structured data).
  • ai -- Returns a pre-formatted text string optimized for LLM context windows.
// AI-formatted snapshot (recommended for agent use)
const snapshot = await client.snapshot({
format: 'ai',
targetId: tab.targetId,
compact: true,
interactive: true,
});

if (snapshot.ok && snapshot.format === 'ai') {
console.log(snapshot.snapshot); // Text for the LLM
console.log(snapshot.refs); // Element refs for interaction
}

// Aria tree snapshot
const ariaSnapshot = await client.snapshot({
format: 'aria',
targetId: tab.targetId,
});

if (ariaSnapshot.ok && ariaSnapshot.format === 'aria') {
for (const node of ariaSnapshot.nodes) {
console.log(`${node.ref} [${node.role}] ${node.name}`);
}
}

Snapshot Options​

OptionTypeDescription
format'aria' | 'ai'Output format
targetIdstringTarget tab (optional)
limitnumberMax number of nodes
maxCharsnumberMax characters in output
refs'role' | 'aria'Ref generation mode
interactivebooleanInclude only interactive elements
compactbooleanCompact output
depthnumberMax tree depth
selectorstringCSS selector to scope snapshot
framestringFrame selector
labelsbooleanInclude labels
mode'efficient'Efficient mode
profilestringBrowser profile name

BrowserSession (CDP)​

For direct browser control, use BrowserSession with a CDP WebSocket URL. This requires playwright-core as a dependency.

import { BrowserSession } from 'wunderland/advanced/browser';

const session = new BrowserSession({
cdpUrl: 'ws://localhost:9222/devtools/browser/abc-123',
connectTimeoutMs: 10000,
});

Connection Lifecycle​

// Connect (retries up to 3 times with backoff)
const browser = await session.connect();

// Get or create a page
const page = await session.getPage();

// Get page by target ID
const specificPage = await session.getPageByTargetId('TARGET_ID');

// List all pages across all contexts
const allPages = await session.getAllPages();

// Disconnect
await session.disconnect();

The session automatically handles reconnection deduplication. If multiple callers invoke connect() simultaneously, they share a single connection attempt.

Page State Tracking​

Every page gets automatic tracking for console messages, page errors, and network requests.

const page = await session.getPage();
const state = session.getPageState(page);

if (state) {
console.log('Console messages:', state.console.length);
console.log('Page errors:', state.errors.length);
console.log('Network requests:', state.requests.length);
}

State is stored in a WeakMap keyed by the Playwright Page instance, so it is automatically garbage collected when the page is closed.

Limits are enforced to prevent memory issues:

  • Console messages: max 500
  • Page errors: max 200
  • Network requests: max 500

Role Refs for Element Targeting​

After taking a snapshot, store role refs so that BrowserInteractions can target elements by ref.

// Store refs from a snapshot result
session.storeRoleRefs(page, snapshotResult.refs, 'role');

// Get a locator for a ref
const locator = session.refLocator(page, 'e1');
await locator.click();

Ref formats supported:

  • e1 -- Direct ref
  • @e1 -- Prefix notation
  • ref=e1 -- Explicit notation

BrowserInteractions API​

BrowserInteractions wraps BrowserSession to provide high-level, agent-friendly methods.

import { BrowserInteractions, BrowserSession } from 'wunderland/advanced/browser';

const session = new BrowserSession({ cdpUrl: 'ws://...' });
await session.connect();

const interactions = new BrowserInteractions(session);

Click​

await interactions.click({
ref: 'e1',
// Optional:
doubleClick: false,
button: 'left', // 'left' | 'right' | 'middle'
modifiers: ['Control'], // Alt, Control, Meta, Shift
targetId: 'TAB_TARGET_ID',
timeoutMs: 30000,
});

Hover​

await interactions.hover({ ref: 'e3' });

Type​

await interactions.type({
ref: 'e2',
text: 'Hello, Wunderland!',
slowly: false, // Use pressSequentially with 50ms delay
submit: true, // Press Enter after typing
});

Fill Form​

Batch-fill multiple form fields at once.

await interactions.fillForm({
fields: [
{ ref: 'e10', value: 'Alice', type: 'text' },
{ ref: 'e11', value: 'researcher', type: 'select' },
{ ref: 'e12', value: 'true', type: 'checkbox' },
],
});

Select Option​

await interactions.selectOption({
ref: 'e5',
values: ['option-2'],
});

Press Key​

await interactions.pressKey({ key: 'Escape' });
await interactions.pressKey({ key: 'Tab', delayMs: 100 });

Scroll Into View​

await interactions.scrollIntoView({ ref: 'e20' });

Screenshot​

// Full page screenshot
const buffer = await interactions.screenshot({ fullPage: true });

// Element screenshot by ref
const elementShot = await interactions.screenshot({ ref: 'e5' });

// Element screenshot by CSS selector
const selectorShot = await interactions.screenshot({ element: '.main-content' });

Wait​

// Wait for time
await interactions.wait({ timeMs: 2000 });

// Wait for text to appear
await interactions.wait({ text: 'Loading complete' });

// Wait for text to disappear
await interactions.wait({ textGone: 'Loading...' });

// Wait for URL change
await interactions.wait({ url: '**/dashboard' });

// Wait for load state
await interactions.wait({ loadState: 'networkidle' });

// Wait for a JavaScript condition
await interactions.wait({ fn: 'document.readyState === "complete"' });
await interactions.goto({ url: 'https://example.com/page' });

Get Page Info​

const url = await interactions.getUrl();
const title = await interactions.getTitle();

Evaluate JavaScript​

// Run JS in the page context
const result = await interactions.evaluate<string>({
fn: 'return document.title',
});

// Run JS on a specific element
const text = await interactions.evaluate<string>({
ref: 'e5',
fn: 'return el.textContent',
});

Complete Example: Agent Browsing Flow​

import { BrowserClient, BrowserSession, BrowserInteractions } from 'wunderland/advanced/browser';

// Option A: HTTP client approach (browser running as a service)
const client = new BrowserClient({ baseUrl: 'http://localhost:9222' });
await client.start();
const tab = await client.openTab('https://news.ycombinator.com');
const snapshot = await client.snapshot({
format: 'ai',
targetId: tab.targetId,
compact: true,
});

// The snapshot text can be sent directly to the LLM
// for the agent to decide what to do next.

// Option B: Direct CDP approach (local Playwright control)
const session = new BrowserSession({
cdpUrl: 'ws://localhost:9222/devtools/browser/...',
});
await session.connect();

const interactions = new BrowserInteractions(session);
await interactions.goto({ url: 'https://news.ycombinator.com' });
await interactions.wait({ loadState: 'networkidle' });

const page = await session.getPage();
const state = session.getPageState(page);
console.log(`Loaded with ${state?.requests.length} network requests`);

// Clean up
await session.disconnect();