A website command is an installed app script. A skill teaches Chat when and how to call it. The Browser capability supplies the page operations. Chat, an app view, and Scheduler all start the same Script service. There is no separate command registry, website.run API, terminal CLI, or OpenCLI runtime dependency.
For a multi-step scraper, network responses, page state, or debugging, use
the raw Browser tools. App scripts can send target-local
CDP commands and read events in their own session with browser.debug.
The recipe APIs below remain a simpler option for DOM lists and modal reads.
flowchart LR
Author[Chat authoring skill] --> VFS[/apps source and /skills instructions]
VFS --> Installed[Reviewed and installed package]
Chat[Chat: scripts.run] --> Scripts[Script service]
View[App view: scripts.run] --> Scripts
Scheduler[Schedule: script plus input] --> Scripts
Installed --> Scripts
Scripts --> Browser[browser.extract / browser.workflow]
Browser --> Site[Private temporary site tab]
Scripts --> Output[Run result and app collections]
Browser API
browser.inspect({url, scope?, limit?}) returns observed CSS selectors, tags,
roles, text and selected attributes. Scope defaults to body; limit defaults
to 100, up to 200. Multiple or missing scopes return source_changed. Output
is bounded and can be truncated. It reads the top-level document only.
browser.extract({recipe, inputs}) runs a version 1 recipe. These read APIs need
browser.read on the existing Workspace browser resource. They create a
private temporary tab with Redac's browser login and release it on every exit.
They do not adopt another app or Chat tab. Only host DOM readers execute in an
isolated world; the SDK receives no WebContents, cookies, tokens or eval API.
A recipe has these closed fields:
export default {
version: 1,
name: 'productPrice',
description: 'Read the observed product offer',
origin: 'https://shop.example',
urlTemplate: 'https://shop.example/products/{productId}',
inputs: ['productId'],
ready: '#product',
rows: '#product',
fields: [
{ name: 'title', selector: 'h1', read: 'text', required: true },
{ name: 'price', selector: '.price', read: 'text', required: true },
{ name: 'seller', selector: '.seller', read: 'text', required: false },
],
limit: 1,
maxPages: 1,
// empty: '.no-products',
// login: '#sign-in-required',
// next: 'a[rel=next]',
};
These are illustrative selectors. Use browser.inspect to obtain actual selectors and browser.extract to verify them. Every declared input is required and appears as a URL placeholder; extra inputs fail. Values are URL encoded. Origin cannot be an input. Fields use text (rendered innerText), textContent (for a verified accessibility copy), or attribute. Allowed attributes are href, src, aria-label, title and datetime. A missing field selector reads its row. Required fields must have one nonempty match; ambiguity fails even for optional fields. Missing optional fields return null. Keep fields narrow: textContent on a large container may include scripts or hidden content.
Rows must be visible. A field may use a hidden accessibility copy within its visible row. Limits are 20 fields, 100 total rows and 10 pages. A next selector must identify one observed link with href. Repeated URLs and origin changes fail. A limit reached before exhaustion returns incomplete. A real empty result needs the recipe's visible empty-state selector. Readiness waits within the command's 20 second total deadline.
The result contains state, rows, complete, pages, sourceUrl, checkedAt and issues. State is ok, empty, incomplete, source_changed or login_required. Timeout and blocked navigation are SDK errors. Required-field failures return no rows; incomplete explicitly returns bounded partial rows. Do not report partial rows as all available data. No automatic selector fallback is used.
Origin checks cover top-level navigation and DOM reads. They do not stop the website's normal cross-origin subresource loads. Access remains the existing broad Browser connection, not a site-specific grant. Current Access and the bound caller lifetime are checked during execution. Revocation and script cancellation close the temporary tab.
Open a modal before extraction
Use browser.workflow({recipe, inputs, steps}) when the page needs a click,
such as View Receipt, before its fields appear. Chat calls browser_workflow;
views and scripts call the same SDK entry. It is a mutation that requires
browser.interact (Click and type) on Workspace browser. That action covers
the click sequence and its extracted result. Read page access alone cannot
run it. Native submit controls are refused. Other site click handlers can have
side effects, so use this interaction permission only for intended actions.
const result = await cap.call('browser.workflow', {
recipe: receiptRecipe,
inputs: { receiptId: input.receiptId },
steps: [{ action: 'click', selector: '#view-receipt', ready: '#receipt-dialog' }],
});
These selectors are examples. Observe the actual button and modal in Chat's
browser before testing and packaging them. The recipe's ready, rows, and
fields describe the opened modal. Up to ten steps run in order on the first
page. Each click matches exactly one visible, enabled element, then waits for
exactly one visible ready element. Missing targets or content wait within
the existing twenty-second total deadline. Ambiguous, disabled, invalid, or
submit targets return source_changed with no rows. An observed explicit
submit must use the separate Chat Submit flow with owner approval.
The workflow keeps the same private app-owned tab, Redac login, exact origin,
bounded extraction, cancellation, revocation, and cleanup as extraction.
It accepts JSON steps, not caller page code. Form fill, frames, and network
response capture are not part of this API. Store the tested steps with the
recipe in the installed package. A schedule runs the same script and requires
the app's browser.interact access to be Allow for unattended execution.
Package a command
Use a format version 2 manifest, with actions including browser.read for extraction or browser.interact for workflows, and a script such as collect. Store the recipe in recipe.js alongside collect.js. The package executes immutable installed bytes, so it needs no VFS read grant for its recipe. Do not read a mutable VFS recipe on each scheduled run.
import recipe from './recipe.js';
export default async (input, { cap, signal }) => {
signal.throwIfAborted();
const result = await cap.call('browser.extract', {
recipe,
inputs: { productId: input.productId },
});
signal.throwIfAborted();
// A collection is preferred for many rows. This small command returns JSON.
const rowsJson = JSON.stringify(result.rows);
if (rowsJson.length > 60000) throw new Error('Result is too large.');
return { state: result.state, complete: result.complete, rowsJson };
};
Declare productId as a bounded string in inputSchema. The example resultSchema has state (bounded string), complete (boolean), and rowsJson (string, maxLength 60000), all required, with additionalProperties false. Script and collection schemas accept scalar values only. See apps and SDK for complete manifests, records, script views and schedules.
Chat uses scripts_catalog to get the installed input schema and revision, scripts_run to start it with a UUID requestKey, and scripts_get to read the run result. Exact retries reuse the key. scripts_cancel cancels accepted work. Chat needs scripts.run on scripts:<app-id>. The app needs its own Browser Access. Other apps start Off. Results sent to the model use the normal Harness PII policy; an approved app's SDK and records can still contain real data.
A view calls client.cap.call('scripts.run', {appId, definitionRevision, script, input, requestKey}). Chat's capability tools use the explicit installed app id and a current definitionRevision. VFS drafts cannot run through Script service.
For a schedule, target {kind: 'script', script: 'collect', input: {productId}}
on the installed app. Chat uses schedules_create, then
schedules_get to obtain the current version and schedules_setEnabled only when requested. New schedules start paused.
The creator needs Allow on scripts.run and schedules.manage for this app
resource. The app needs Allow for unattended browser reads; Ask needs approval. A
schedule runs without AI unless the script explicitly calls agent.send.
Create a calling skill
After verifying the installed command, Chat saves a valid skill with name and description frontmatter at /skills/<site>-<command>/SKILL.md. Include the actual app id, script name, input semantics, the list/start/get flow and result checks. The existing Pi VFS source discovers it next turn with skills read Allow. The skill can guide repair and create more scripts using these fixed host APIs; it cannot create native capabilities or permissions.
The workflow follows OpenCLI's separation of browser use and adapter creation, with observed selectors, field verification and site knowledge. See OpenCLI's authoring skill.