Connect with CDP

View as Markdown

The CDP connection is a Run-scoped browser connection. Starting a browser Runtime opens a Run and returns a credentialed cdpUrl. Use that URL to drive the browser yourself while BCTRL records the same Run for observability.

The URL is valid only while its Run is active. Treat it as a credential: do not log it, persist it, or expose it to page code.

Get the connection URL

An ephemeral Runtime starts during creation, so its connection is returned on the create response:

1const runtime = await bctrl.runtimes.create({
2 name: "checkout-job",
3});
4
5if (!runtime.connection) {
6 throw new Error("Runtime did not start");
7}
8
9const { cdpUrl, runId } = runtime.connection;

For a reusable profile-backed Runtime, create it stopped and start it when you need a new session:

1const runtime = await bctrl.runtimes.create({
2 name: "customer-portal",
3 profile: true,
4});
5
6const started = await bctrl.runtimes.start(runtime.id);
7const { connection, runId } = started;
8const { cdpUrl } = connection;

The start response contains:

FieldTypeAlways presentMeaning
runtimeIdstringYesRuntime that owns the browser.
runIdstringYesRun for this browser session.
connection.cdpUrlstringYesRun-scoped CDP endpoint.
connection.webDriverUrlstringYesWebDriver endpoint for the same browser Run.
startedbooleanYesWhether this call opened a new Run.
connection.recording{ enabled: boolean }YesWhether recording is enabled.

Calling start() for an already-active Runtime reuses its active Run and returns the same connection URL. When the Run ends, the URL stops working. A later Run receives a new URL.

Playwright

1import { chromium } from "playwright";
2
3const browser = await chromium.connectOverCDP(cdpUrl);
4const context = browser.contexts()[0];
5const page = context.pages()[0] ?? (await context.newPage());
6
7await page.goto("https://example.com");
8console.log(await page.title());
9
10await browser.close();
11await bctrl.runtimes.stop(runtime.id);

The Runtime owns the browser lifecycle. Close the client connection when your work is complete, then stop the Runtime.

Puppeteer

1import puppeteer from "puppeteer-core";
2
3const browser = await puppeteer.connect({
4 browserWSEndpoint: cdpUrl,
5});
6
7const pages = await browser.pages();
8const page = pages[0] ?? (await browser.newPage());
9
10await page.goto("https://example.com");
11console.log(await page.title());
12
13await browser.disconnect();
14await bctrl.runtimes.stop(runtime.id);

Use disconnect() with Puppeteer because the Runtime, not Puppeteer, owns the browser process.

Connection ownership

Only one external CDP controller can be connected to a Run at a time. Close the current CDP connection before connecting another client.

BCTRL’s live Views, recordings, traces, and events do not consume the external CDP slot. Hosted Tools and Agent turns use the same browser, so coordinate page-level actions when an external client and hosted automation are active at the same time.

Stop the session

Stopping the Runtime ends its active Run and invalidates the connection URL:

1const stopped = await bctrl.runtimes.stop(runtime.id);
2console.log(stopped.runId, stopped.status, stopped.stopped);

Use the runId with Runs to inspect trace spans, raw events, recordings, usage, and files produced by the session.

Next