Docs

Page Code API

tab.evaluate() runs a function of yours inside a page, and tab.waitFor(fn) polls one until it returns something truthy. Both need the page-js grant. Use them for whatever the SDK form methods don’t cover: reading data fields, text and attr don’t expose, or a page that needs behavior they don’t have.

Why it exists

The SDK form methods, reading ones such as text and attr included, cover most of what a script does on a page, without any code of yours running there. They can’t cover everything a web page can be: a table to read in one go, a list that loads more as you scroll, a canvas. Page code is the general tool for those.

It’s also the most powerful thing a script can do on a page. Your function runs as you on that site: it can read what the page can read, including its cookies and storage, and send requests as you. That is why it has its own grant, which the approval prompt spells out. Reach for an SDK form method, or a custom parts map, first.

How it works

tab.evaluate(fn, ...args) sends your function to the page and calls it there. It receives the page as its first argument, page, then the arguments you pass, and returns plain JSON. Everything outside the function runs in your script:

// Tranquil automation. Rules for writing and changing it: ~/.tranquil/llm/writing-automations.md
// @permissions page-js sites=example.com
import { tabs } from "tranquil/automation";

const tab = await tabs.active();
const headings = await tab.evaluate(
  (page, selector: string) => Array.from(page.document.querySelectorAll(selector), (h) => h.textContent ?? ""),
  "h2",
);
console.log(headings);
  • It needs page-js, and runs as you on that site.
  • The function is sent as source. It can’t use variables from your script; pass what it needs as arguments, which must be JSON. The same goes for waitFor(fn), which takes no arguments.
  • It runs in the page’s main world, alongside the page’s own scripts, not in the SDK’s isolated world (see Two worlds, one page).
  • console.log inside it goes to that tab’s DevTools, not the Runs panel.
  • It works on strict Content-Security-Policy sites, because it goes through the browser’s debugger rather than the page’s script-src.
  • It may return a promise. The call waits for it, up to the 30-second limit on every call to the browser.

Typing

The arguments you pass are checked against the function’s parameters, and the result’s type is what the function returns. Name the result’s type when you want to state it, as in evaluate<string[]>(fn, "h2"); the function’s return is then checked against it, but the arguments aren’t. A string expression (evaluate("document.title")) can’t be checked at all, so its result is unknown unless you name it, and the opaque-page-code lint rule flags it.

The Page declaration

page is typed by the SDK’s Page declaration, which lists every web API page code may use:

  • Exhaustive: a bare document or window in a script is a type error, so page code can only reach the page through page, and every web API a script touches is visible in its source.
  • Visible: the sensitive groups (requests made as you, cookies and storage, navigation, the clipboard) are listed in the approval prompt, and the page-free-global lint rule flags page code that reaches a browser global without page..

Examples

Read what text and attr don’t expose

A table read in one call, as rows of text:

// Tranquil automation. Rules for writing and changing it: ~/.tranquil/llm/writing-automations.md
// @permissions page-js sites=example.com
import { context, tabs } from "tranquil/automation";

const tab = await tabs.active();
const rows = await tab.evaluate((page) =>
  Array.from(page.document.querySelectorAll("table tbody tr"), (tr) =>
    Array.from(tr.querySelectorAll("td"), (td) => (td.textContent ?? "").trim()))
);
await context.result({ rows });

Wait on the page itself

waitFor(fn) polls your function until it returns something truthy, for a condition no target describes:

// Tranquil automation. Rules for writing and changing it: ~/.tranquil/llm/writing-automations.md
// @permissions page-js sites=example.com
import { tabs } from "tranquil/automation";

const tab = await tabs.active();
await tab.waitFor((page) => page.document.querySelectorAll("li.result").length >= 20);

Behavior the SDK form methods don’t have, in a module of your own

A list that loads more rows as its end scrolls into view. The scrolling and the waiting happen in the page; the module keeps going until it has enough rows or the list stops growing:

// load-rows.ts, a module (not an automation, so no header)
import type { Tab } from "tranquil/automation";

/** Scroll a list that loads as it goes until it holds `count` rows; returns how many it holds. */
export async function loadRows(tab: Tab, selector: string, count: number): Promise<number> {
  let shown = 0;
  for (;;) {
    const before = shown;
    shown = await tab.evaluate((page, sel: string, had: number) =>
      new Promise<number>((resolve) => {
        const rows = () => page.document.querySelectorAll(sel);
        rows()[rows().length - 1]?.scrollIntoView({ block: "end" });
        const started = Date.now();
        const check = () => {
          if (rows().length > had || Date.now() - started > 5000) resolve(rows().length);
          else page.setTimeout(check, 100);
        };
        check();
      }), selector, before);
    if (shown >= count || shown === before) return shown;
  }
}
// Tranquil automation. Rules for writing and changing it: ~/.tranquil/llm/writing-automations.md
// @permissions page-js read=load-rows.ts sites=example.com
import { context, tabs } from "tranquil/automation";
import { loadRows } from "./load-rows.ts";

const tab = await tabs.active();
await context.result({ rows: await loadRows(tab, "li.result", 100) });

A script declares each module of yours it imports in read=, as it declares any file it reads.

Page code vs custom parts

Which one a field needs:

yesnoyesno: it needs behaviorDo fill, clear and fields() work on the field?Nothing to addDoes the field work normally,with only its parts in non-standard places?A custom parts mapPage code

A custom parts map and page code both deal with a page that isn’t built to standards, and they overlap: page code could read a dropdown’s choice too. They differ in what you hand the SDK and what you keep:

Custom parts mapPage code (tab.evaluate)
What you writeData: CSS selectors and a patternA function
GrantNone of its own: tabs to read, forms to fill, as fill, clear and fields() needpage-js: your code runs as you on the site, and can read its cookies and storage and send requests as you
Where it runsThe SDK’s isolated world only looks your selectors upThe page’s main world, beside the site’s scripts
Entering a valueTrusted input, then the value is read backWhatever your code does, usually setting the value directly, with no read-back
Guarantees keptVerified targets, read-back, the submit guard, sites=, secret redaction, an action log entry per callOne evaluate entry in the action log; none of the form guarantees apply inside it
Check Custom PartsHeld back like any fill, with its parts checkedRuns for real
Offline testsfieldsOf(html, { customParts })Can’t run offline
Approval promptThe grants fill, clear and fields() needpage-js, and the sensitive web APIs your code uses

The rule of thumb:

  • A map when the field works normally (you open it, pick an option, and it shows your choice), and the only problem is that the SDK form methods can’t find its parts, because the page puts them where HTML and ARIA don’t describe them: the choice in a <div> beside the input, the menu opened from a wrapper around it.
  • Page code when the page needs behavior the SDK form methods don’t have.
  • Never page code just to set a value a map would let fill set. fill reads the value back and refuses to submit without submit; your own code does neither.

Where page code stops

  • Check Custom Parts doesn’t hold it back. Page code runs for real during a check, including anything it changes on the page.
  • No form guarantees apply inside it. No read-back, no submit guard, no secret redaction; the action log records one evaluate call and its outcome.
  • No offline tests. It needs a live page.
  • Everything it touches is in its source. Through the Page declaration, a reviewer sees every web API it uses, and the approval prompt lists the sensitive ones.