Runtimes

View as Markdown

A runtime is a remote resource — a browser, desktop, or spreadsheet — running on BCTRL infrastructure inside a workspace. You don’t run anything locally. Your code sends commands over the network and gets results back.

Today, BCTRL supports browser runtimes. Desktop and spreadsheet runtimes are coming.

Launching a browser runtime

Every runtime needs an alias (a name you choose) and a driver (how you want to interact with it):

1const runtime = await workspace.runtimes
2 .browser("main")
3 .playwright({ mode: "ephemeral" });

The alias "main" identifies this runtime within the workspace. You can run multiple runtimes concurrently — each with its own alias.

Drivers

Pick the driver that matches your automation style:

Playwright

Full Playwright API — page.goto(), page.locator(), page.evaluate(). Best for structured automation with strong selector support.

Puppeteer

Puppeteer API — familiar if you’re coming from Chrome DevTools Protocol workflows.

Stagehand

AI-native driver — stagehand.act(), stagehand.extract(), stagehand.observe(). Best for agents that reason about page content rather than using selectors.

Selenium

WebDriver protocol — driver.get(), driver.findElement(). Familiar to enterprise automation teams.

1// Playwright
2const pw = await workspace.runtimes.browser("a").playwright();
3
4// Puppeteer
5const pp = await workspace.runtimes.browser("b").puppeteer();
6
7// Stagehand
8const sh = await workspace.runtimes.browser("c").stagehand();
9
10// Selenium
11const se = await workspace.runtimes.browser("d").selenium();

All four drivers connect to the same remote browser — the driver just determines the API surface you use to control it.

Launch modes

Ephemeral

A fresh browser with no saved state. Destroyed when the runtime stops.

1const runtime = await workspace.runtimes.browser("quick").playwright({
2 mode: "ephemeral",
3});

Ephemeral with config

Customize the browser’s behavior:

1const runtime = await workspace.runtimes.browser("stealth").playwright({
2 mode: "ephemeral",
3 config: {
4 humanize: true,
5 solveCaptchas: true,
6 },
7 extensionIds: ["captcha-solver-ext"],
8});

Profile-backed

Use a persistent browser profile with saved cookies, storage, and fingerprint:

1const runtime = await workspace.runtimes.browser("crm").playwright({
2 mode: "profile",
3 profileId: "sales-bot",
4});

Profiles are managed via bctrl.browserProfiles and persist across runtime restarts.

Working with a runtime

Once launched, the runtime gives you the driver’s native API surface:

1// Navigate
2await runtime.page.goto("https://example.com");
3
4// Extract
5const title = await runtime.page.title();
6const html = await runtime.page.content();
7
8// Interact
9await runtime.page.locator("#search").fill("bctrl");
10await runtime.page.locator("#submit").click();
11
12// Screenshot
13const screenshot = await runtime.page.screenshot();

See Browser Capabilities for the full reference.

AI agent execution

Runtimes with Stagehand or browser-use support agent-level commands:

1// Stagehand — action by instruction
2await runtime.stagehand.act("Click the login button");
3
4// Stagehand — structured extraction
5const data = await runtime.stagehand.extract(
6 "Extract the product name and price",
7 z.object({ name: z.string(), price: z.string() })
8);
9
10// Browser-use — autonomous agent
11const op = await runtime.browserUse.agent.execute(
12 "Export the latest invoices as CSV"
13);

Long-running agent calls return an operation that you can poll or wait on:

1const result = await workspace.operations.wait(op.id, {
2 until: "completed",
3});

Viewing and embedding

Get a live interactive view or a recording of a runtime:

1// Live view — interactive or view-only
2const live = await runtime.live({ interactive: true });
3console.log(live.url);
4
5// Recording — replay after the fact
6const recording = await runtime.recording();
7console.log(recording.url);

Multiple concurrent runtimes

A workspace can run multiple runtimes simultaneously:

1const [crm, email, research] = await Promise.all([
2 workspace.runtimes.browser("crm").playwright({ mode: "profile", profileId: "crm-bot" }),
3 workspace.runtimes.browser("email").playwright({ mode: "profile", profileId: "email-bot" }),
4 workspace.runtimes.browser("research").playwright({ mode: "ephemeral" }),
5]);
6
7// Each runtime operates independently
8await crm.page.goto("https://crm.example.com");
9await email.page.goto("https://mail.example.com");
10await research.page.goto("https://google.com");

Base runtime methods

These methods are available on every runtime regardless of driver. They work the same across Playwright, Puppeteer, Stagehand, and Selenium.

run(request)

Run structured automation steps directly against the runtime. This is the low-level interface that all driver methods use under the hood.

1const result = await runtime.run({
2 steps: [
3 { call: "page.goto", args: ["https://example.com"] },
4 { call: "page.screenshot" },
5 ],
6});
ParameterTypeRequiredDescription
stepsExecuteStep[]YesArray of steps to execute
idempotencyKeystringNoDeduplication key for retries
mode'fail-fast'NoStop on first error

Each step has a call (method name), optional args (array), optional ref (target ID), and optional bind (alias for the result). Steps execute in order.

Returns WorkspaceExecuteResponse

stop()

Stop the runtime and release infrastructure. Profile-backed runtimes save their state before stopping.

1await runtime.stop();

Returns Runtime — the final runtime state.

live(options?)

Get an embeddable live view of the browser. Can be interactive (control the browser) or view-only.

1const live = await runtime.live({ interactive: true });
2console.log(live.iframeUrl);
ParameterTypeRequiredDescription
interactivebooleanNoAllow mouse/keyboard interaction (default: false)

Returns { iframeUrl: string }

recording()

Get a recording replay of the runtime’s session.

1const recording = await runtime.recording();
2console.log(recording.iframeUrl);

Returns { iframeUrl: string }

state()

Query the current state of the runtime — which pages are open, the current URL, driver info.

1const state = await runtime.state();

Returns WorkspaceStateResponse

events.list(query?)

List events that have occurred in the runtime — navigation, console logs, errors, page creation.

1const events = await runtime.events.list({
2 eventTypes: ["page.navigated", "console"],
3 limit: 50,
4});
ParameterTypeRequiredDescription
afterstringNoCursor — return events after this ID
limitnumberNoMax events to return
eventTypesstring[]NoFilter by event type
pageIdstringNoFilter by page ID

events.wait(request?, options?)

Wait for an event matching the given criteria. Blocks until a match or timeout.

1const event = await runtime.events.wait({
2 eventTypes: ["page.navigated"],
3 timeoutMs: 10_000,
4});
ParameterTypeRequiredDescription
afterstringNoWait for events after this cursor
timeoutMsnumberNoMax wait time in milliseconds
eventTypesstring[]NoEvent types to match
pageIdstringNoFilter by page ID

captcha.detect(options?)

Detect captchas present on the current page.

1const result = await runtime.captcha.detect();
2if (result.detected) {
3 console.log(result.type); // e.g., "recaptcha", "hcaptcha"
4}

captcha.solve(options?)

Attempt to solve a detected captcha.

1const result = await runtime.captcha.solve();

stagehand.act(instruction, options?)

Execute a natural-language instruction.

1await runtime.stagehand.act("Click the login button");

stagehand.extract(instruction, schema, options?)

Extract structured data from the page using natural language.

1import { z } from "@bctrl/sdk";
2
3const data = await runtime.stagehand.extract(
4 "Extract the product name and price",
5 z.object({ name: z.string(), price: z.string() })
6);

stagehand.observe(instruction?, options?)

Observe the page and identify interactive elements.

1const elements = await runtime.stagehand.observe("Find all clickable buttons");

stagehand.agent(config?)

Create a Stagehand agent for multi-step autonomous tasks.

1const agent = runtime.stagehand.agent();
2const result = await agent.execute("Navigate to settings and export data as CSV");

stagehand.getMetrics() / stagehand.getHistory()

1const metrics = await runtime.stagehand.getMetrics();
2const history = await runtime.stagehand.getHistory();

browserUse.agent(config?).execute(task, options?)

Run an autonomous browser-use agent.

1const op = await runtime.browserUse.agent().execute(
2 "Export the latest invoices as CSV"
3);
4
5const result = await workspace.operations.wait(op.id, {
6 until: "completed",
7});

browserUse.codeAgent(config?).execute(task, options?)

Run a browser-use code agent (generates and executes code).

1const op = await runtime.browserUse.codeAgent().execute(
2 "Scrape all product listings into a structured table"
3);