PandaScript
A PandaScript is a web automation script that can be run directly by Lightpanda browser.
No environment to set up like NodeJS or Python, no Puppeteer/Playwright to write, no CDP serialization cost, no LLM required: just vanilla Javascript with a small set of native primitives.
Use normal JavaScript variables, functions, loops, objects, arrays, JSON.parse, JSON.stringify, and other standard ECMAScript built-ins.
It’s reproducible, deterministic and token-free (no LLM required).
To run a PandaScript:
lightpanda run <my_script>.jsRuntime Environment
Agent scripts run in their own V8 context. That context is separate from the web page’s JavaScript context.
- It is not the browser page environment. There is no
window,document, DOM,localStorage,navigator, or page global state in the agent script. Read page data withpage.extract(...), or explicitly run page JavaScript withpage.evaluate(...)when that is the right tool. - It is not Node.js. There is no
require,process,fs,path, npm package loading, command-line argument API, or Node network/filesystem API. - Page scripts cannot see agent variables or Lightpanda primitives. Agent scripts cannot directly see page variables.
page.evaluate(...)runs JavaScript in the page context, distinct from the agent context’s own nativeeval. It is not a global; it’s a method on the page object.- Agent variables persist for the lifetime of one script run, across
navigations and primitive calls. A later
lightpanda run script.jsstarts with a fresh agent context. gotois the one asynchronous primitive: alwaysawait page.goto(...). Every other page method (extract,evaluate,click,fill, and the rest) is synchronous and blocking. The script body runs inside an async wrapper, so top-levelawaitis allowed, unlike a plain classic script.- Tool failures throw JavaScript
Errorexceptions and stop execution unless you catch them. - The script’s output is whatever it
returns from the top level (objects and arrays printed as JSON; other values coerced). End a script withreturn page.extract({ ... });orreturn results;. A bare trailing expression is not printed.console.log(...)is for extra or debug output and does not JSON-format objects.
The agent context includes a small console object, split across the two
output streams. Find every method in the
PandaScript reference.
console.log("printed to stdout");
console.error("printed to stderr");Values And Return Types
Most primitives return the browser tool’s result text as a JavaScript string.
page.extract(...) is the exception: it returns extracted data as a normal
JavaScript value, so local script logic can use it directly. The result mirrors
your schema: an object schema returns an object keyed by your fields (even with
a single field), and a bare array schema returns an array:
const page = new Page();
await page.goto("https://news.ycombinator.com/");
const data = page.extract({
title: "title",
stories: [{
selector: "tr.athing",
limit: 5,
fields: {
id: { attr: "id" },
title: ".titleline > a"
}
}]
});
return data; // printed automatically as JSONDestructure when a single field is all you need:
const { stories } = page.extract({
stories: [{
selector: "tr.athing",
limit: 5,
fields: {
id: { attr: "id" },
title: ".titleline > a"
}
}]
});
for (const story of stories) {
console.log(story.title);
}page.evaluate(...) still returns the page evaluate tool result text. When it
returns an object or array, that text is JSON.
Primitive arguments must be JSON-serializable. Strings, numbers, booleans,
arrays, plain objects, and null work. Find what the other values throw in the
PandaScript reference.
Installed Primitives
Only recorded browser primitives are installed globally: new Page(), then
goto, extract, evaluate, screenshot, the interaction methods and the
waitFor* methods as methods on the page. Find the full table, the calling
conventions and the timeouts in the PandaScript reference.
Navigation
Use goto(...) to navigate to an URL:
const page = new Page();
await page.goto("https://example.com");
await page.goto({
url: "https://example.com/app",
timeout: 15000
});The call resolves to the same page object (await page.goto(url) === page)
once it reaches waitUntil (load by default), and rejects if navigation
fails or times out ("navigation timed out"). Pass waitUntil: "networkidle"
(or another state) to wait past load directly, or follow with
waitForState(...) / waitForSelector(...) when completeness matters.
Structured Extraction
Use page.extract(...) to read data from the current page without writing page-side
JavaScript. This is the preferred bridge from page content into local agent
logic.
const result = page.extract({
heading: "h1",
links: [{
selector: "a",
limit: 10,
fields: {
text: "",
href: { attr: "href" }
}
}]
});Find every schema form in the PandaScript reference.
Every value is a string (trimmed text or a raw attribute) or null; parse
numbers in script logic. An array field that matches nothing yields []
without complaint (a page with zero comments is a valid result), but if
every field in the schema misses, page.extract(...) throws
no schema selector matched any element; treat that as “my selectors are
wrong”, not “the page is empty”.
page.extract(...) reads only the current page. For list-to-detail scraping:
capture the list, then loop in the script (goto each row’s URL and extract
the detail). The local agent context keeps the data across navigations, so the
assembly happens in plain JavaScript. See the complete
example below.
Use local variables to keep extracted data available to later script logic:
const data = page.extract({ title: "title" });Page JavaScript
page.evaluate(...) is the explicit escape hatch into the current page’s JavaScript
context. Its script string runs where window and document exist.
await page.goto("https://example.com");
const title = page.evaluate("document.title");
console.log(title);Keep the boundary clear:
const selector = "h1";
// Good: local agent logic builds an extract schema.
const data = page.extract({ heading: selector });
// Bad: page evaluate cannot see local agent variables.
page.evaluate("document.querySelector(selector).textContent");page.evaluate(...) cannot call page.goto, page.extract, or other agent
primitives. Agent scripts cannot access document directly. If you need page
DOM data, prefer page.extract(...); use page.evaluate(...) only for page
behavior that extraction cannot express.
page.waitForScript(...) also evaluates in the page context, repeatedly, until the
expression is truthy or the timeout expires:
page.waitForScript("document.querySelectorAll('.row').length >= 5");Interaction Primitives
The action primitives operate on the current page. Most take one object whose fields match the browser tool schema:
page.click({ selector: "a.login" });
page.fill({ selector: "input[name='acct']", value: "$LP_HN_USERNAME" });
page.fill({ selector: "input[name='pw']", value: "$LP_HN_PASSWORD" });
page.press({ key: "Enter" });
page.waitForSelector("#logout");
page.hover({ selector: "#menu" });
page.selectOption({ selector: "select[name='country']", value: "FR" });
page.setChecked({ selector: "input[name='terms']", checked: true });
page.setChecked({ selector: "input[name='newsletter']", checked: false });
page.scroll({ y: 600 });
page.scroll();setChecked defaults checked to true when the field is omitted
(setChecked("#chk") checks the box). press’s leading positional is the
optional selector, not key: a bare press("Enter") binds "Enter" to
selector and fails. Press on the document (not necessarily the focused
element) with press({ key: "Enter" }) or press(null, "Enter"); target an
element with press("#search", "Enter") or
press({ key: "Enter", selector: "#search" }).
$LP_* placeholders in string arguments are resolved inside the Lightpanda
process. This keeps credentials out of recorded scripts and LLM prompts. In
recordings, resolved LP_* values are scrubbed back to placeholders.
Error Handling
Primitive failures throw JavaScript exceptions:
try {
page.waitForSelector({ selector: "#dashboard", timeout: 1000 });
} catch (err) {
console.error("dashboard did not appear:", err.message);
throw err;
}Common failures:
| Error | Meaning |
|---|---|
ReferenceError: document is not defined | You tried to use browser DOM APIs in the agent context. Use page.extract(...) or page.evaluate(...). |
ReferenceError: require is not defined | Agent scripts are not Node.js scripts. |
no page loaded - run page.goto(url) first | A page-dependent primitive ran before navigation. |
invalid arguments | A primitive received the wrong number or shape of arguments, or a non-JSON-serializable value. |
extract: no schema selector matched any element | Every field in the schema missed. Fix the selectors; an empty page section yields null/[] per field, not this error. |
Find the complete list in the PandaScript reference.
Complete Example
This script opens Hacker News, extracts five stories, visits each comments page, and prints one JSON object. The looping and object assembly happen in the local agent script, not in the page.
const HN = "https://news.ycombinator.com";
const page = new Page();
await page.goto(HN);
const { stories } = page.extract({
stories: [{
selector: "tr.athing",
limit: 5,
fields: {
id: { attr: "id" },
title: ".titleline > a",
url: { selector: ".titleline > a", attr: "href" }
}
}]
});
for (const story of stories) {
story.comments = [];
if (!story.id) continue;
await page.goto(`${HN}/item?id=${story.id}`);
const { comments } = page.extract({
comments: [{
selector: "tr.athing.comtr:has(.commtext)",
limit: 3,
fields: {
author: ".hnuser",
text: ".commtext"
}
}]
});
story.comments = comments;
}
return stories; // printed automatically as JSON