← Files WebcmdARCHIVED FILE
skills/webcmd-browser/references/browser-run-playwright.md
7.3 KB · Oct 3, 2026 · 06:31 UTC
# Browser Run Details
This file is only the QuickJS `browser run` contract. Site memory, candidates, and checkpointing are not part of this runtime.
## The runtime is QuickJS, not Node and not the page
`browser run` executes your program in a QuickJS sandbox. `page`, `context`, and `browser`
are Playwright handles that drive a browser running somewhere else — they are not evidence
that you are inside that browser, and not evidence that you are inside Node.
Two consequences produce almost every `browser run` failure:
- **Anything DOM-shaped must go inside `page.evaluate()`.** `document`, `window`, and
`localStorage` are not in your scope. `document.querySelector(...)` at the top level
throws `'document' is not defined`; `await page.evaluate(() => document.querySelector(...))`
works, because that callback is serialized and run in the page.
- **Anything Node-shaped does not exist.** No `require`, no `import` of host modules, no
`fs`, no `Buffer`, no `process`.
Browser state in the bound session persists between runs. JavaScript variables and handles
do not — each run starts with a fresh scope.
## What is available
| Need | Use |
|---|---|
| Drive the page | `page`, `context`, `browser` (Playwright) |
| Read or manipulate the DOM | `page.evaluate(() => …)` |
| Find elements | `page.locator(selector)`, `page.getByRole(...)`, and the other `getBy*` locators |
| Log | `console` |
| Return data | `return` any JSON-compatible value |
`page.$` and `page.$$` work, but prefer `page.locator()` — it retries and auto-waits.
`context.newPage()` works and creates a tab the Webcmd session tracks. You cannot close it
from inside `run` (see below); list tabs with `webcmd --session <session-id> browser tabs`.
Create the owning Session with `webcmd --profile work session create "Work Project"`,
then use the returned immutable, Profile-scoped readable ID, for example
`webcmd --profile work --session work-project-k7 browser tabs`.
`page.snapshotForAI()` is not available; use `webcmd --session <session-id> browser snapshot` instead.
## What is blocked, and what to use instead
These throw `BROWSER_RUN_API_UNSUPPORTED` because page and context ownership belongs to the
Webcmd session, not to your program:
| Blocked | Instead |
|---|---|
| `page.close()` | Leave the tab open, or `webcmd session close <session-id>` |
| `context.close()`, `browser.close()` | `webcmd session close <session-id>` |
| `browser.newContext()` | `webcmd session create <name>` — one run is scoped to one Session |
| `browser.newBrowserCDPSession()`, `context.newCDPSession()` | Not exposed inside `run` |
| `playwright.request` (`newRequest`) | `page.request` for calls in the page's context |
## Files and binary data
There is no host filesystem. Passing a host path to `setInputFiles` fails with
`File paths are unavailable in the QuickJS sandbox; use in-memory file payloads` — supply
`{ name, mimeType, buffer }` with a `Uint8Array` instead.
## Artifacts: getting bytes out of the sandbox
The only way to get a file out of a run is to write it as an artifact, using a **relative
logical filename** — absolute paths and `..` are rejected with `BROWSER_RUN_INVALID_INPUT`.
### Writing one
```js
const receipt = await writeArtifact('report.csv', new TextEncoder().encode(csv), 'text/csv');
return receipt;
```
`writeArtifact(filename, bytes, contentType?)` takes a `Uint8Array` and resolves to the
receipt. `contentType` is optional and defaults to `application/octet-stream` for anything
that is not `.png`/`.jpg` — pass it explicitly when it matters. `__webcmdWriteArtifact` is a
legacy alias for the same function.
Two other calls write artifacts for you: `page.screenshot({ path: 'shot.png' })` and
`download.saveAs('out.csv')`. Both take the same relative logical filename.
### Capturing a download
`download.createReadStream()` throws — Readable streams do not exist in the sandbox. Use
`saveAs` with a relative name instead; it routes through the artifact sink:
```js
const [download] = await Promise.all([
page.waitForEvent('download'),
page.getByRole('button', { name: 'Convert' }).click(),
]);
await download.saveAs(download.suggestedFilename());
return { saved: download.suggestedFilename() };
```
Do not scrape an on-page preview as a substitute for the downloaded bytes — it will not match.
### Redeeming a receipt
Every artifact written during a run appears in the run result's `artifacts` array, whether it
came from `writeArtifact`, `saveAs`, or `screenshot`:
```json
{
"artifactId": "artifact_9d1f6368490aa37a22a18426",
"filename": "downloads/out.csv",
"contentType": "application/octet-stream",
"byteSize": 12,
"locator": "browser-run://artifact_9d1f6368490aa37a22a18426/downloads%2Fout.csv"
}
```
Locally the bytes land at `~/.webcmd/cache/browser-run/<artifactId>/<filename>` (under
`$WEBCMD_CACHE_DIR/browser-run` when that is set), readable once `run` has returned. Hosted
runs use the same receipt shape with a `cloud-artifact://` locator plus an authenticated
HTTPS `downloadUrl`. The receipt never carries the bytes themselves, so return the receipt —
or just read it off `artifacts` — rather than trying to return file contents through `result`.
Download a hosted artifact later with:
```text
webcmd artifact download <download-url> --output <local-path>
```
`--output` is required. Webcmd supplies hosted authentication. Ordinary `curl` is neither
required nor automatically authenticated; the URL alone does not grant access.
## Errors
`BROWSER_RUN_*` errors name invalid input, unsupported Playwright calls, timeouts, output
limits, or serialization failures. A timeout can include
`BROWSER_RUN_SIDE_EFFECTS_MAY_HAVE_OCCURRED`; inspect the page state before retrying a write.
A rejection phrased `QuickJS promise rejected: 'X' is not defined` means `X` is a Node or DOM
global that the sandbox does not provide — check the two tables above before retrying.
## Snapshot behavior
Use `webcmd --session <session-id> browser snapshot --snapshot-mode act` to inspect actionable controls, `--snapshot-mode tree` for fuller page structure, or `--snapshot-mode read` for readable article/content text. Successful runs return `snapshotDiff` automatically and support `--snapshot-mode act|tree`. Pass `--no-snapshot-diff` for research or deterministic inspection when the result returns the exact bounded evidence needed, including navigation followed by targeted extraction or response capture. Navigation alone does not require a diff. Keep the automatic diff for exploratory or state-changing interactions whose outcome is not independently verified by the returned result. If a requested diff exceeds the output ceiling, Webcmd omits it and returns a warning; continue when the explicit result is sufficient, otherwise use a targeted snapshot or extraction. A failed post-run snapshot becomes a warning, not a successful result change.
## Timing
Run results include timing fields such as `quickjs_boot_ms`, `client_bundle_init_ms`, `program_ms`, `browser_wait_ms`, and `snapshot_ms`. `--timeout <seconds>` limits the complete run; `--max-output <characters>` bounds returned data and logs.
## Hosted mode
Hosted `browser run` uses the same QuickJS sandbox and the same rules. Only the browser on
the far end differs — hosted runs drive a Browser Use browser over CDP rather than local
Cloak. Programs that work locally work hosted; the tables above apply in both modes.
SHA-256: b797a4cb29f1200d3e10a6acc65a867081bfd80da8dfc372d12028aace29b5a9