Connect with Playwright
Drive a session's Chrome with Playwright over the Chrome DevTools Protocol, in Node or Python.
Sessions have a Chrome browser unless you create them with "browser": false. The session’s connectUrl is a Chrome DevTools Protocol (CDP) WebSocket. Playwright connects to it with connectOverCDP, and from then on you use Playwright as usual. A session created without a browser has no connectUrl, and the CDP connection answers 409 browser_disabled (see Three kinds of session).
Connect
This is the tested example from the platform’s examples/integrations/playwright: it connects, reads quotes with locators, clicks to the next page, counts the page’s requests in a route and saves a screenshot, on quotes.toscrape.com (a demo site made for scraping practice).
/** * Playwright with Boxline: connect Playwright to a Boxline session's browser over CDP and use it as usual (locators, * routes, screenshots). No AI, no model cost. * * npm install && npx tsx index.ts (BOXLINE_API_KEY in the environment) * * Writes output/result.json and output/page.png. */import { mkdirSync, writeFileSync } from "node:fs";import { join } from "node:path";import { Boxline } from "@boxline/sdk";import { chromium } from "playwright-core";const out = process.env.OUTPUT_DIR ?? "output";const bx = new Boxline();const session = await bx.sessions.create({ timeout: 300, userMetadata: { example: "integrations/playwright" } });console.log(`Session: ${session.id}`);// connectUrl is a signed WebSocket address: treat it like a password.const browser = await chromium.connectOverCDP(session.connectUrl!);try { // The session's browser has one context with one page; use them, or open more with context.newPage(). const context = browser.contexts()[0]!; const page = context.pages()[0] ?? (await context.newPage()); // Routes run in Playwright on this computer: every request the page makes passes through here (block, change or // answer it); this one counts them by type and lets them through. const requests: Record<string, number> = {}; await context.route("**/*", (route) => { const type = route.request().resourceType(); requests[type] = (requests[type] ?? 0) + 1; return route.continue(); }); await page.goto("https://quotes.toscrape.com/"); const quotes = page.locator(".quote"); const first = await quotes.evaluateAll((els) => els.slice(0, 3).map((el) => ({ text: el.querySelector(".text")!.textContent!.trim(), author: el.querySelector(".author")!.textContent!.trim() })), ); await page.getByRole("link", { name: /Next/ }).click(); await page.waitForURL(/\/page\/2\/$/); const page2Author = (await quotes.first().locator(".author").textContent())?.trim(); mkdirSync(out, { recursive: true }); const png = await page.screenshot({ path: join(out, "page.png") }); for (const q of first) console.log(`${q.author}: ${q.text.slice(0, 70)}`); console.log(`Page 2 starts with ${page2Author}; requests seen: ${JSON.stringify(requests)}; screenshot ${png.length} bytes`); writeFileSync(join(out, "result.json"), JSON.stringify({ sessionId: session.id, quotes: first, page2Url: page.url(), page2Author, requests, screenshotBytes: png.length }, null, 2));} finally { await session.stop(); await browser.close().catch(() => undefined);}"""Playwright with Boxline: connect Playwright to a Boxline session's browser over CDP and use it as usual (locators,routes, screenshots). No AI, no model cost. pip install -r requirements.txt && python main.py (BOXLINE_API_KEY in the environment)Writes output/result.json and output/page.png. No browser download is needed: `playwright install` is not requiredto connect to a remote browser."""import jsonimport osimport refrom pathlib import Pathfrom playwright.sync_api import sync_playwrightfrom boxline import Boxlineout = Path(os.environ.get("OUTPUT_DIR", "output"))bx = Boxline()with bx.sessions.create(timeout=300, user_metadata={"example": "integrations/playwright"}) as session, sync_playwright() as p: print(f"Session: {session.id}", flush=True) # connect_url is a signed WebSocket address: treat it like a password. browser = p.chromium.connect_over_cdp(session.connect_url) # The session's browser has one context with one page; use them, or open more with context.new_page(). context = browser.contexts[0] page = context.pages[0] if context.pages else context.new_page() # Routes run in Playwright on this computer: every request the page makes passes through here (block, change or # answer it); this one counts them by type and lets them through. requests = {} def count(route): requests[route.request.resource_type] = requests.get(route.request.resource_type, 0) + 1 route.continue_() context.route("**/*", count) page.goto("https://quotes.toscrape.com/") quotes = page.locator(".quote") first = quotes.evaluate_all( "els => els.slice(0, 3).map((el) => ({ text: el.querySelector('.text').textContent.trim(), author: el.querySelector('.author').textContent.trim() }))" ) page.get_by_role("link", name=re.compile("Next")).click() page.wait_for_url(re.compile(r"/page/2/$")) page2_author = quotes.first.locator(".author").text_content().strip() out.mkdir(parents=True, exist_ok=True) png = page.screenshot(path=str(out / "page.png")) for q in first: print(f"{q['author']}: {q['text'][:70]}") print(f"Page 2 starts with {page2_author}; requests seen: {requests}; screenshot {len(png)} bytes") result = {"sessionId": session.id, "quotes": first, "page2Url": page.url, "page2Author": page2_author, "requests": requests, "screenshotBytes": len(png)} (out / "result.json").write_text(json.dumps(result, indent=2, ensure_ascii=False))In Node, playwright-core is enough, and no playwright install is needed: the browser runs in the session, not on your computer. In Python, install playwright.
Use the default context
Work in browser.contexts()[0] rather than calling browser.newContext(). Your pages then share the browser state that browser profiles, and stop and resume, carry over. A page in a new context does not see a profile's cookies.
Disconnecting
Disconnecting matters for browser-only sessions: without keepAlive, they stop about 5 seconds after the last CDP client leaves. Set keepAlive: true to connect several times, and stop the session yourself when you are done. See Lifecycle.
If the platform replaces the session’s machine (see Checkpoints and a lost machine), connect again to the same connectUrl. Connecting to a stopped session fails with 409 session_not_running: resume it first, then connect to its fresh connectUrl.
Downloads
Files the browser downloads land in /workspace/downloads in the session, not on your computer. Wait for one with session.files.waitFor("downloads/*.csv"), then read it. See File uploads and downloads.
Mix Playwright with the API
You can drive the same session with Playwright and with the actions API at the same time. Use Playwright for complex flows, and actions or plain-English steps for quick, latency-sensitive steps. To run Playwright code right next to the browser instead of over the internet, use Scripts.
To match the browser’s clock and language to the country its proxy comes out in, or to set them yourself, create the session with "browser": { "mode": "realistic" }. See Browser mode.
Install and run
The packages are pinned at the versions the example’s check ran with. It writes to output/ and stops its session when it finishes.
# Node 22 or newerexport BOXLINE_API_KEY=… # your Boxline API keynpm install @boxline/sdk playwright-core@1.63.0 tsx@4.23.15npx tsx index.ts# Python 3.10 or newerexport BOXLINE_API_KEY=… # your Boxline API keypip install boxline-sdk playwright==1.63.0python main.pyWhat its check proves
It checks that Playwright read the first 3 quotes (Einstein first) with locators, clicked Next to page 2 (Marilyn Monroe), saw the page's requests in a route, and saved its screenshot. To run it from a clone of the examples (git clone https://github.com/Boxline-dev/examples.git), go to examples/integrations/playwright, run the commands above, then check the result from the same folder: npx tsx check.ts output.
Source: github.com/Boxline-dev/examples/integrations/playwright on GitHub. See all integrations.
These docs describe a beta API. Endpoints, fields and SDK methods may change before general availability; breaking changes will be listed in the changelog.