state object. Variables, pages, and listeners persist between calls. Browser tabs are shared, but state is not.playwriter session new # => 1 playwriter session new # => 2 # List all sessions with their stored keys playwriter session list # ID State Keys # -------------- # 1 page, userData # 2 -
-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 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 = 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())
context.pages() returns all browser tabs with Playwriter enabled, shared across all sessions. Multiple agents see the same tabs. There is no default page. Always create your own page:// First call: create a tab and navigate immediately state.page = await context.newPage() await state.page.goto('https://example.com') // Use state.page for ALL subsequent operations
if (!state.page || state.page.isClosed()) { state.page = await context.newPage() } await state.page.goto('https://example.com')
context.pages() if the user points you to a specific tab. Find it by URL. findLast picks the most recently opened match:state.page = context.pages().findLast((p) => p.url().includes('myapp.com')) if (!state.page) throw new Error('No myapp.com page found')
playwriter by default. Use the shortest clear single-word name with no spaces, such as docs, shop, test, or scrape.# Tabs created by this session join a group named "docs" playwriter session new --tab-group docs # Pick the group color yourself (any Chrome color) playwriter session new --tab-group docs --tab-group-color blue # Rename the group later — moves the session's existing tabs playwriter session update 1 --tab-group research # Change only the color — also works on the default playwriter group playwriter session update 1 --tab-group-color red
playwriter session new --tab-group scrape --tab-group-color grey
--tab-group. That keeps many open tabs readable instead of one mixed strip:playwriter session new --tab-group agent1 --tab-group-color blue playwriter session new --tab-group agent2 --tab-group-color pink
playwriter session new --tab-group done --tab-group-color grey
playwriter group only moves tabs that session created. Manually enabled tabs and other sessions' tabs stay in the default group. Renaming a custom group moves everything in it, including tabs you dragged in.--tab-group-color, each custom group gets a deterministic color derived from its name (the default playwriter group stays green). The flag accepts every Chrome group color: grey, blue, red, yellow, green, pink, purple, cyan, orange. playwriter session list shows the group per session in the GROUP column.connectViaExtension() from the playwriter package. It starts the relay if needed, waits for the Chrome extension, creates a session, and connects over CDP.import { connectViaExtension } from 'playwriter' await using connection = await connectViaExtension({ tabGroup: 'email-check', tabGroupColor: 'grey', }) const page = await connection.browser.contexts()[0].newPage() await page.goto('https://example.com') // connection closes automatically when the scope ends, also on throw
tabGroupColor is typed as Chrome's tab group colors: grey, blue, red, yellow, green, pink, purple, cyan, orange. await using calls close() at scope end: it closes the tabs this connection opened (never other sessions' tabs), disconnects CDP, and deletes the session. It needs Node 24+ (or a TypeScript 5.2+ / tsx build). Without await using, call await connection.close() in finally.closeTabsOnEnd: false to keep the opened tabs after close(). A later process finds them in context.pages():// process A await using a = await connectViaExtension({ tabGroup: 'job', closeTabsOnEnd: false }) await (await a.browser.contexts()[0].newPage()).goto('https://example.com') // process B, later await using b = await connectViaExtension({ tabGroup: 'job' }) const page = b.browser.contexts()[0].pages().findLast((p) => p.url().includes('example.com'))
b.close() never closes tabs that b did not open, so the reused tab stays open.selectBrowser. It gets every connected browser and returns the one to use. Without it, connecting fails when more than one browser is connected.import { connectViaExtension, listBrowsers } from 'playwriter' await using connection = await connectViaExtension({ selectBrowser: (browsers) => { // [{ id, browser: 'Brave', email: 'me@work.com', activeTabs, playwriterVersion }] return browsers.find((b) => b.browser === 'Brave' && b.email === 'me@work.com') }, }) console.log(connection.extensionBrowser.id) // same list, without connecting const browsers = await listBrowsers()
id is stable across restarts, so you can save it and match on it later. email is null when the profile is not signed in./cli/session/new yourself. Do not call chromium.connectOverCDP(getCdpUrl(...)) unless you already have a session from the CLI.# Reset a session (reconnect browser, clear state) playwriter session reset 1 # Delete a session playwriter session delete 1 # List all sessions playwriter session list
| Variable | Description |
state | Persisted object, isolated per session |
context | Browser context, access all pages via context.pages() |
require | Load Node.js modules (path, fs, crypto, etc.) |
fetch | Standard fetch API |
Buffer, URL, URLSearchParams | Standard globals |
setTimeout, setInterval | Timers |
crypto, process | Node.js globals |
page global. Reading page throws. Store your own page in state.page.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()
[WARNING] New page opened from current page (index N, initial url: ...) message pointing to the new tab.state.page.removeAllListeners()
browser.close() or context.close(). Only close pages you created yourself.