Skip to content

Website functions and skills

Collect data from a website with a function.

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.

Synced from the Redac Desktop developer guide on 2026-10-30 · SDK contract version 23