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.loginside 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
documentorwindowin a script is a type error, so page code can only reach the page throughpage, 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-globallint rule flags page code that reaches a browser global withoutpage..
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:
flowchart TD
Q1["Do fill, clear and fields() work on the field?"]
Q1 -->|yes| NONE(["Nothing to add"])
Q1 -->|no| Q2["Does the field work normally,<br/>with only its parts in non-standard places?"]
Q2 -->|yes| MAP(["A custom parts map"])
Q2 -->|"no: it needs behavior"| CODE(["Page 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 map | Page code (tab.evaluate) | |
|---|---|---|
| What you write | Data: CSS selectors and a pattern | A function |
| Grant | None of its own: tabs to read, forms to fill, as fill, clear and fields() need | page-js: your code runs as you on the site, and can read its cookies and storage and send requests as you |
| Where it runs | The SDK’s isolated world only looks your selectors up | The page’s main world, beside the site’s scripts |
| Entering a value | Trusted input, then the value is read back | Whatever your code does, usually setting the value directly, with no read-back |
| Guarantees kept | Verified targets, read-back, the submit guard, sites=, secret redaction, an action log entry per call | One evaluate entry in the action log; none of the form guarantees apply inside it |
| Check Custom Parts | Held back like any fill, with its parts checked | Runs for real |
| Offline tests | fieldsOf(html, { customParts }) | Can’t run offline |
| Approval prompt | The grants fill, clear and fields() need | page-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
fillset.fillreads the value back and refuses to submit withoutsubmit; 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
evaluatecall and its outcome. - No offline tests. It needs a live page.
- Everything it touches is in its source. Through the
Pagedeclaration, a reviewer sees every web API it uses, and the approval prompt lists the sensitive ones.