Migrate to BAP
BAP borrows Puppeteer's method names in TypeScript and Playwright's in Python, but it isn't a drop-in replacement for either: it runs over BrowserQL instead of a live CDP session, so anything that needs direct browser control has no equivalent. This guide covers what changes, what doesn't carry over, and what BAP adds that neither library has.
- An existing Puppeteer or Playwright script
- A Browserless API token from your account dashboard
- The BAP Quickstart if you haven't installed and connected yet
Should you migrate?
Migrate to BAP when you want BrowserQL's managed stealth, proxies, and CAPTCHA solving without hand-writing GraphQL, and your script doesn't rely on page.mouse, page.keyboard, frames, or browser contexts. If it does, keep the script as-is and connect it to Browsers as a Service (BaaS) over CDP instead.
Method differences
- TypeScript
- Python
Page borrows Puppeteer's method names, but some shared methods behave differently:
| Method | Puppeteer | BAP |
|---|---|---|
$eval(selector, fn) | Runs your function against the matched element and returns its result | Returns the text content of the matched selector. No function argument |
$$eval(selector, fn) | Runs your function against all matched elements | Delegates to mapSelector and returns structured MapSelectorResponse[] |
evaluate(fn, ...args) | Passes serialized arguments and returns deserialized results | Accepts a string or function, always returns string | null, and passes no arguments |
waitForRequest / waitForResponse | Accept a URL string or a predicate function | Accept a URL string or an options object. No predicate functions |
scroll() | Not on Page. You use mouse.wheel() or evaluate() | First-class method with selector and coordinate targeting |
Page borrows Playwright's method names, but some shared methods behave differently:
| Method | Playwright | BAP |
|---|---|---|
eval_on_selector(selector, expression) | Runs expression against the matched element and returns its result | Returns the text content of the matched selector. No expression argument |
eval_on_selector_all(selector, expression) | Runs expression against every matched element | Delegates to map_selector and returns a list of MapSelectorResponse dicts |
evaluate(expression, arg) | Passes a serialized arg and returns the deserialized result | Accepts only an expression string, always returns str | None, and takes no arg |
wait_for_selector(selector, state=...) | Accepts a state predicate (attached, visible, hidden, detached) | Accepts visible/hidden booleans, no attached/detached states |
| Typing into a field | fill() sets the value instantly; press_sequentially() types key by key | type() is the only entry point, with a per-character delay range for human-like typing |
What has no BAP equivalent
- TypeScript
- Python
These Puppeteer APIs have no BAP equivalent, because they need a live CDP session:
- Input devices: no
page.keyboard,page.mouse, orpage.touchscreen - Frames: no
page.frames(),page.mainFrame(), or frame targeting - Workers: no
page.workers() - Function exposure: no
exposeFunction() - Emulation: no
emulate()oremulateCPUThrottling(), thoughemulateMediaType()is supported - Security and cache: no
setBypassCSP(),setCacheEnabled(), orsetOfflineMode() - Coverage, tracing, and accessibility: no
page.coverage,page.tracing, orpage.accessibility
These Playwright APIs have no BAP equivalent, because they need a live CDP session:
- Browser contexts: no
new_context().Browser.new_page()opens directly against the BrowserQL endpoint - Input devices: no
page.keyboardorpage.mouse - Frames: no
page.frames,page.main_frame, or frame targeting - Function exposure: no
expose_function() - Generic interception: no
page.route(). Usefulfill(),reject(),request(), andresponse()instead, which match filters server-side rather than running a Python callback per request - Tracing and accessibility: no
page.context.tracingor accessibility snapshots
When only one step needs the missing API
Migrating isn't all-or-nothing. Two escape hatches keep the rest of the script on BAP:
- Hand the session to Puppeteer or Playwright.
reconnect()returns a CDPbrowserWSEndpointfor the same live browser, so BAP can handle the stealth, proxying, and CAPTCHA work, then your existing CDP code drives the step that needspage.mouseor frames. Nothing is re-run and no state is lost. - Send the mutation yourself. For a BrowserQL capability the SDK doesn't wrap yet,
page.send()runs any BQL document on the current session. This doesn't help with APIs that need direct CDP access, since BrowserQL has no equivalent either.
What BAP adds
- TypeScript
- Python
BAP adds methods Puppeteer has no equivalent for, and each one has a guide:
| Methods | Guide |
|---|---|
html(), text(), markdown(), mapSelector() | Extracting content |
reject(), fulfill(), request()/response() | Network control |
proxy() | Stealth & humanization |
solve(), solveImageCaptcha() | Solving CAPTCHAs |
reconnect() | Reconnecting to sessions |
liveURL(), stopSessionRecording(), switchToWindow() | Live URLs & recording |
check()/uncheck() are checkbox helpers, preferences() sets session-wide defaults such as the timeout, and loadSecret() fills a credential from a 1Password integration without exposing it to your script.
BAP adds methods Playwright has no equivalent for, and each one has a guide:
| Methods | Guide |
|---|---|
html(), text(), markdown(), map_selector() | Extracting content |
reject(), fulfill(), request()/response() | Network control |
proxy() | Stealth & humanization |
solve(), solve_image_captcha() | Solving CAPTCHAs |
reconnect() | Reconnecting to sessions |
live_url(), stop_session_recording(), switch_to_window() | Live URLs & recording |
check()/uncheck() are checkbox helpers, preferences() sets session-wide defaults such as the timeout, and load_secret() fills a credential from a 1Password integration without exposing it to your script.
FAQ & Troubleshooting
Can I reuse my existing script unchanged?
Not without changes. BAP covers the subset of Puppeteer or Playwright that BrowserQL can express, so anything using page.mouse, page.keyboard, frames, browser contexts, or page.route() needs rewriting. If only a step or two needs those APIs, hand the session to your existing CDP code with reconnect(). If you'd rather not change the script at all, use BaaS over CDP instead.
Why does evaluate() return a string when my function returns an object?
evaluate() always resolves to a string (or null/None), and it takes no arguments. Serialize inside the page with JSON.stringify() and parse the result yourself.