Browser use
A workflow can open a real browser, drive it with plain code, and hand it to an agent. The browser runs inside the run's own machine, and your program owns it: you can navigate, fill a form, or read the page in deterministic code, then give a logged-in browser to an agent() leaf that operates the app from there.
Open a browser you own
computer.openBrowser() returns a session handle. It is a live in-VM browser, not a remote one, and the handle belongs to your program:
import { computer, agent } from "@boardwalk-labs/workflow";
const browser = await computer.openBrowser({ startUrl: "https://app.example.com" });
// Drive it in code...
await browser.navigate("https://app.example.com/reports");
if (!(await browser.url()).includes("/reports")) throw new Error("navigation failed");
// ...or hand it to an agent for the open-ended part.
await agent("Export the Q3 revenue report as CSV and save it.", { session: browser });
await browser.close(); // or let the run reap it at the endThe same browser is shared: the program drives it, the agent drives it, and both see the same tabs and the same session. That is the point of a handle the program holds rather than a browser the agent conjures on its own.
Set it up before the agent sees it
The most common reason to open a browser in code first is to get past the parts an agent should never touch: a login wall, a cookie banner, a workspace picker. Because your program is the trusted layer (the only place a secret lives), it can authenticate with a real credential and then hand over a browser that is already inside the app:
import { computer, agent, secrets } from "@boardwalk-labs/workflow";
const browser = await computer.openBrowser({ startUrl: "https://app.example.com/login" });
// Log in yourself. The password is in the program; it is never put in a prompt.
const password = await secrets.get("APP_PASSWORD");
await browser.eval('document.querySelector("#email").value = "ops@example.com"');
await browser.eval('document.querySelector("#password").value = ' + JSON.stringify(password));
await browser.eval('document.querySelector("form").submit()');
if (!(await browser.url()).includes("/dashboard")) throw new Error("login failed");
// Now the agent gets an authenticated browser, and never saw the credential.
await agent("Find the failing invoice for Acme and download it.", { session: browser });This split is deliberate. The agent operates the application, but the sharp edges (a password, arbitrary page JavaScript) stay in code the model cannot reach. secrets.getis fail-closed: only names you declared in the workflow's permissions.secrets resolve.
What the program can drive
The session handle exposes the browser to deterministic code. Every method round-trips to the live page, so you can act, assert, and inspect between agent turns:
| Method | What it does |
|---|---|
navigate(url) | Go to a URL and wait for it to load. |
url(), title() | Read where the page is, to branch or assert in code. |
eval(js) | Run JavaScript in the page and get the result. Program-only, never an agent tool. |
screenshot() | Capture the page; stored as an artifact and its ref returned. |
console(), network() | Read console messages and network requests, the highest-signal way to verify a page. |
close() | Tear the browser down. Idempotent, and automatic at run end if you skip it. |
Hand it to an agent
Pass the session to a leaf and the agent gains a set of browser actions bound to that one browser: navigate, click, type, read the page, check the console and network:
await agent("Reproduce the checkout bug from ticket #812, then describe what breaks.", {
session: browser,
});The agent works from the page's underlying structure, not from screenshots, so it runs on any model you route to without needing vision. One leaf drives one browser; to work two screens at once, open two sessions and give each to its own parallel agent call.
It survives a wait
The browser lives inside the run's machine, so it is still open, still logged in, and on the same tab when the run resumes from a human gate or a sleep. You can open a browser, log in, pause for an approval that takes a day, and pick up exactly where you left off, with no re-login and no reconnect logic:
const browser = await computer.openBrowser({ startUrl: "https://admin.example.com" });
await signIn(browser);
const ok = await humanInput({
prompt: "Approve this refund batch?",
input: { kind: "choice", options: ["Approve", "Cancel"] },
});
// Hours later, on resume: same browser, still signed in.
if (ok.value === "Approve") {
await agent("Process the approved refunds in the admin panel.", { session: browser });
}One browser, one run
A session is a per-run resource, not a durable one: it lives and dies within a single run. A program can open several browsers when it needs to (each is real memory, so it is a cost you choose), and any left open are reaped when the run finishes. There is nothing to clean up if a run crashes.
Secrets stay out of the page
Browser use widens what a run touches, so the boundaries matter. Two hold by construction:
- The model never sees your secrets. Credentials live in program code and are redacted from everything an
agent()leaf reads, so a hostile page cannot phish them out of the model. - Arbitrary page JavaScript is program-only.
eval()is the trusted layer's tool, not the agent's. The model gets structured browser actions, not a way to run whatever code a page suggests.
Each run gets its own isolated machine, so a browsing run cannot reach another run's state. Anything the browser displays, including a credential shown on screen, is part of the run's record and inherits its retention.