briqbriq
Guides

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 agentDrive it withOne call costs
Has only briq's tools (ChatGPT, a chat app)briq_exec and the browser's /function APIA new browser: 1.5 to 2 s, plus the pages
Runs code (Claude Code, Codex CLI, a server)Playwright or Puppeteer, over a WebSocket1 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) };
}
  1. curl sends the script to the browser. $TOKEN is already in the briq's environment.
  2. The briq launches a browser, runs the function with a Puppeteer page, then closes the browser.
  3. 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.pdf

Then 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/playwright as 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=20000 in ms) or split the task.
  • The first call on a new briq is slow, up to about 30 seconds: Chromium's first launch.
  • 401 from a private URL: the briq_token cookie is missing. A token in the URL's query string does not work for a WebSocket: the client follows the redirect without the cookie.
  • 400 when connecting: an address with a query parameter the browser does not know. It takes token, timeout, launch, blockAds, trackingId and profile.
  • page.cookies() never returns inside /function. Use Network.getCookies, as above.
  • Calls get slower and slower on small or medium: steady work uses up a shared CPU's allowance. Use size: "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 small are slower each.
  • A tool call has about a minute: most MCP clients stop waiting after 60 seconds.
  • The script's page is Puppeteer's, not Playwright's, in /function.
  • A WebSocket counts as one visit when it opens: a session longer than the briq's sleep needs a longer one.
  • Logs show warnings and errors only, never the token.

How can we help?

Message to the briq team
hello@briq.cloud