For AI agents: a documentation index is available at /llms.txt
Skip to main content

Dropping down to raw BrowserQL

Every BAP method is a typed wrapper around one BrowserQL mutation, and page.send() is the door past the wrappers: it runs any GraphQL document you hand it on the same session. Reach for it when a BQL mutation has no BAP method yet, when a new server feature lands before the SDK release that wraps it, or when you want several steps in one round trip.

Prerequisites

Run a raw mutation

send() takes a GraphQL document string and resolves with the operation's data object. It rides the page's existing WebSocket, so whatever your typed calls did before it, the raw mutation sees: same page, same cookies, same session.

import Browserless from "@browserless.io/bap-ts";

const TOKEN = "YOUR_API_TOKEN_HERE";

const browser = Browserless.connect({
browserWSEndpoint: "wss://production-sfo.browserless.io/chromium/bql",
token: TOKEN,
});

const page = await browser.newPage();

// The generic parameter types the resolved data object.
const data = await page.send<{ goto: { status: number } }>(
`mutation Visit($url: String!) {
goto(url: $url) { status }
}`,
{ variables: { url: "https://example.com" } },
);

console.log(data.goto.status);
await browser.close();

Response:

200

Pass values through variables instead of interpolating them into the document. The server coerces and validates variables by type, and the operation string stays constant, which also keeps user input from turning into query syntax.

Errors behave like every other BAP call: GraphQL errors reject with BrowserQLError, and the optional timeout option bounds the operation like any typed method, as covered in error handling.

Batch steps into one round trip

Typed methods run one mutation per call. A raw document can carry several fields, and BrowserQL executes them in order inside a single operation, which cuts latency on chatty sequences and guarantees no client-side gap between steps:

// One round trip: navigate, wait, and extract.
const data = await page.send<{
goto: { status: number };
waitForSelector: { time: number };
html: { html: string };
}>(`mutation ScrapeInOneTrip {
goto(url: "https://example.com") { status }
waitForSelector(selector: "h1") { time }
html(selector: "main") { html }
}`);

console.log(data.html.html.length);

The BrowserQL schema lists every mutation and its arguments, and the BQL IDE is the fastest place to prototype a document before pasting it into send().

Observe raw frames (TypeScript)

Every frame the transport receives, including server-pushed frames that don't answer a request, is visible through page.on('message'). subscribe() is the same listener with an unsubscribe function returned:

const page = await browser.newPage();

// Every incoming frame, including pushes.
page.on("message", (frame) => {
console.log("frame:", frame);
});

// Same stream, unsubscribe-style.
const unsubscribe = page.subscribe((frame) => console.log(frame));

await page.send('mutation { goto(url: "https://example.com") { status } }');

unsubscribe();
// page.off("message", listener) removes a named listener too.

// The demo page holds a socket and a concurrency slot like any other.
await page.close();

One protocol fact to respect: BrowserQL is serial request/response, so a pushed frame that arrives while a request is in flight is consumed as that request's response. on('message') shows you everything, but correlating subscription traffic beyond that is up to your custom transport.

FAQ & Troubleshooting

How do I know what mutations and arguments exist?

The BrowserQL schema reference documents every mutation, argument, and response type. BAP's generated method table maps each SDK method to its source mutation, so anything in the schema without a method is send() territory.

My document works in the BQL IDE but send() rejects it

Check that the variables you pass match the declared types exactly, including required ! markers. A BrowserQLError from send() carries the server's GraphQL errors, which name the argument or field that failed validation.

Does send() bypass BAP's timeouts?

No. The connection-level default applies, and the timeout option on send() overrides it per call, exactly like a typed method.

Can Python observe raw frames too?

Frame-level listeners are TypeScript-only. Python's send() covers running raw documents, and page events like console and error are available through page.on() in both SDKs.

Next steps

Was this page helpful?