Drive a browser
Open pages, fill forms, take screenshots and PDFs with a headless Chromium, from an agent that can run code or one that cannot.
Recipe: Headless Chrome
The chrome recipe runs a headless Chromium on its own briq. There are two ways to drive it. Pick the one your agent can use:
| Your agent | Drive it with | One call costs |
|---|---|---|
| Has only briq's tools (ChatGPT, a chat app) | briq_exec and the browser's /function API | A new browser: 1.5 to 2 s, plus the pages |
| Runs code (Claude Code, Codex CLI, a server) | Playwright or Puppeteer, over a WebSocket | 1 to 2 s to connect, then about 10 ms an action |
First, connect your agent, then start the browser:
briq_run({ recipe: "chrome" })
→ chrome running · BROWSER_WS_ENDPOINT=ws://…:3000?token=… · BROWSERLESS_TOKEN=…With briq_exec
briq_exec posts a Puppeteer script to the browser's /function API, inside the briq:
briq_exec({
briq: "bq_…",
cmd: ["sh", "-c", "curl -s -X POST \"http://[::1]:3000/function?token=$TOKEN\" -H 'Content-Type: application/javascript' --data-binary @-"],
stdin: "<the script below>"
})export default async function ({ page }) {
await page.goto('https://httpbin.org/forms/post');
await page.type('input[name=custname]', 'Ada');
await page.click('input[value=medium]');
await Promise.all([page.waitForNavigation(), page.click('form button')]);
return { url: page.url(), text: await page.$eval('body', (b) => b.innerText) };
}curlsends the script to the browser.$TOKENis already in the briq's environment.- The briq launches a browser, runs the function with a Puppeteer
page, then closes the browser. - The function's return value is the answer: a string as text, an object as JSON.
Screenshots, PDFs and downloads
curl -s -X POST "http://[::1]:3000/screenshot?token=$TOKEN" -H 'Content-Type: application/json' \
-d '{"url":"https://example.com"}' -o /tmp/page.png
curl -s -X POST "http://[::1]:3000/pdf?token=$TOKEN" -H 'Content-Type: application/json' \
-d '{"url":"https://example.com"}' -o /tmp/page.pdfThen briq_files_get({ briq, path: "/tmp/page.png" }) shows you the screenshot. A PDF comes back
as base64, up to 1 MB. /content answers with the page's HTML. /download takes a function, like
/function, and answers with the file the page downloaded.
Inside a function, return binary as base64: await page.screenshot({ encoding: 'base64' }).
Keep a session between calls
Each call gets a new browser, so cookies and open pages are gone. Do a task in one function when you can: sign in, act, return. Across calls, return the cookies and send them back:
// Call 1, saved with `curl … -o /tmp/cookies.json`
export default async function ({ page }) {
await page.goto('https://example.com/login');
// … sign in …
const cdp = await page.createCDPSession();
return (await cdp.send('Network.getCookies')).cookies;
}// Call 2, sent as JSON: { "code": "<this script>", "context": { "cookies": <the file> } }
export default async function ({ page, context }) {
await page.setCookie(...context.cookies);
await page.goto('https://example.com/account');
return await page.title();
}Build that JSON with node, which the image has:
node -e 'const f = require("fs"); process.stdout.write(JSON.stringify({ code: f.readFileSync("/tmp/next.js", "utf8"), context: { cookies: JSON.parse(f.readFileSync("/tmp/cookies.json", "utf8")) } }))' \
| curl -s -X POST "http://[::1]:3000/function?token=$TOKEN" -H 'Content-Type: application/json' --data-binary @-With Playwright or Puppeteer
From another briq of the same stack, connect to BROWSER_WS_ENDPOINT:
import { chromium } from 'playwright-core';
const browser = await chromium.connectOverCDP(process.env.BROWSER_WS_ENDPOINT);
const page = await browser.contexts()[0].newPage();
await page.goto('https://example.com');- Puppeteer:
puppeteer.connect({ browserWSEndpoint: BROWSER_WS_ENDPOINT }). - Playwright's own protocol:
chromium.connect()on the same address with/chromium/playwrightas the path. - One script, one browser: the browser closes when the script disconnects. To keep a sign-in
for the next script, save it (
context.storageState()) and load it again.
From outside briq
Publish the browser with a private URL:
briq_run({ recipe: "chrome", expose: "private" })
→ URL=https://chrome-ab12.briq.run/?briq_token=…Send the URL's token as a cookie, and the browser's own token in the address:
const browser = await chromium.connectOverCDP(
`wss://chrome-ab12.briq.run/?token=${BROWSERLESS_TOKEN}`,
{ headers: { cookie: `briq_token=${BRIQ_TOKEN}` } },
);Puppeteer takes the same headers in connect. Each action then also travels between your
machine and the briq's region.
When it does not work
Request has timed out: the call ran past 50 seconds. A selector never matched, a page never finished loading, or it was the first call on a new briq. Set a limit per call (&timeout=20000in ms) or split the task.- The first call on a new briq is slow, up to about 30 seconds: Chromium's first launch.
401from a private URL: thebriq_tokencookie is missing. A token in the URL's query string does not work for a WebSocket: the client follows the redirect without the cookie.400when connecting: an address with a query parameter the browser does not know. It takestoken,timeout,launch,blockAds,trackingIdandprofile.page.cookies()never returns inside/function. UseNetwork.getCookies, as above.- Calls get slower and slower on
smallormedium: steady work uses up a shared CPU's allowance. Usesize: "large", a dedicated CPU. - A site refuses the browser: it is a plain Chromium, with no proxies and no stealth mode.
Good to know
- Four browsers at once, ten more calls queued, then refused. Several at once on
smallare slower each. - A tool call has about a minute: most MCP clients stop waiting after 60 seconds.
- The script's
pageis Puppeteer's, not Playwright's, in/function. - A WebSocket counts as one visit when it opens: a session longer than the briq's
sleepneeds a longer one. - Logs show warnings and errors only, never the token.