Agent-readable docs index: /llms.txt. Download /docs.zip to grep all markdown files locally.

Multi-Agent Browser Sessions with Isolated State

Sessions let you run multiple agents at once without interference. Each session is an isolated sandbox with its own state object. Variables, pages, and listeners persist between calls. Browser tabs are shared, but state is not.

Creating sessions

playwriter session new # => 1 playwriter session new # => 2 # List all sessions with their stored keys playwriter session list # ID State Keys # -------------- # 1 myPage, userData # 2 -
Pass -s <id> to all commands to use a specific session:
playwriter -s 1 -e "state.users = ['Alice', 'Bob']" playwriter -s 2 -e "console.log(state.users)" # undefined (isolated)

State persistence

The state object persists between execute calls within the same session. Use it to store pages, data, listeners, and anything else you need across calls:
// Call 1: store data state.page = context.pages().find(p => p.url() === 'about:blank') ?? await context.newPage() await state.page.goto('https://example.com') state.results = [] // Call 2: data is still there console.log(state.results.length) // 0 state.results.push(await state.page.title())

Pages are shared, state is not

context.pages() returns all browser tabs with Playwriter enabled, shared across all sessions. Multiple agents see the same tabs. To avoid interference, always get your own page:
// First call: grab an empty tab or create one, navigate immediately state.page = context.pages().find(p => p.url() === 'about:blank') ?? await context.newPage() await state.page.goto('https://example.com') // Store in state.page and use it for ALL subsequent operations
Navigate in the same call as creating the page. This prevents another agent from grabbing the same about:blank tab between execute calls.

Handle page closures

The user may close your tab. Always check before using it:
if (!state.page || state.page.isClosed()) { state.page = context.pages().find(p => p.url() === 'about:blank') ?? await context.newPage() } await state.page.goto('https://example.com')

Using existing pages

Only use a page from context.pages() if the user explicitly asks you to control a specific tab:
const pages = context.pages().filter(x => x.url().includes('myapp.com')) if (pages.length === 0) throw new Error('No myapp.com page found') state.targetPage = pages[0]

Session management

# Reset a session (reconnect browser, clear state) playwriter session reset 1 # Delete a session playwriter session delete 1 # List all sessions playwriter session list

Context variables

Every session has access to these globals:
VariableDescription
statePersisted object, isolated per session
pageDefault page (shared, prefer state.page)
contextBrowser context, access all pages via context.pages()
requireLoad Node.js modules (path, fs, crypto, etc.)
fetchStandard fetch API
Buffer, URL, URLSearchParamsStandard globals
setTimeout, setIntervalTimers
crypto, processNode.js globals

Popup handling

Popup windows (window.open, OAuth login flows, target=_blank) are auto-relocated to tabs in the main window by the extension. The new tab appears in context.pages():
await state.page.locator('button:has-text("Login with Google")').click() await state.page.waitForTimeout(1000) // New tab is the last page const pages = context.pages() const loginPage = pages[pages.length - 1] await loginPage.locator('[data-email]').first().click()
You'll receive a [WARNING] New page opened from current page (index N, initial url: ...) message pointing to the new tab.

Clean up

Always clean up listeners at the end of your message to prevent memory leaks:
state.page.removeAllListeners()
Never call browser.close() or context.close(). Only close pages you created yourself.