Pilo: self-hosted automation
Pilo is the open-source automation engine behind Tabstack. Run browser automation on your own machine, with your own model provider, when hosted /automate does not fit.
Hosted /automate runs a browser task for you and returns the result. It works on public websites and cannot log in, and the browser runs on Tabstack’s infrastructure.
Pilo is the other path. It is the open-source automation engine, Apache-2.0, at mozilla/pilo. You install it, it drives a browser you control, and it talks to a model provider you choose. Nothing about the automation runs through Tabstack unless you ask it to.
Which one to use
Section titled “Which one to use”Hosted /automate | Pilo | |
|---|---|---|
| Where the browser runs | Tabstack infrastructure | Your machine |
| Which model drives it | Managed, inside the call | Yours, including local models through Ollama |
| Site scope | Public websites, cannot log in | Whatever the browser you control can reach |
| What you operate | An API call | Node runtime, browser, provider key, config |
| Interface | SDK, CLI, HTTP | CLI and JavaScript library |
| Streamed events | Yes, typed SSE | Event emitter plus result object |
| License | Hosted service | Apache-2.0 |
Reach for hosted /automate when the completed task is what matters and the site is public. Reach for Pilo when you need the browser and the model under your own control, or when the flow will not work without a session the hosted browser cannot have.
Install
Section titled “Install”Requires Node.js 22 or newer.
npm install -g @tabstack/piloOr run it without installing:
npx @tabstack/pilo <command>Then configure a provider. The wizard walks through picking one and entering a key, and writes to ~/.config/pilo/config.json (%APPDATA%/pilo/config.json on Windows).
pilo config initRun a task:
pilo run "what's the weather in Tokyo?"Model providers
Section titled “Model providers”Pilo does not ship a model. You point it at one:
| Provider | Notes |
|---|---|
| Ollama | Local models. Requires Ollama running locally. |
| OpenAI | Key from platform.openai.com |
| OpenRouter | Key from openrouter.ai |
| Google Generative AI | Key from ai.google.dev |
| Vertex AI | Google Cloud, needs project setup and authentication |
# Local model through Ollamapilo config set provider ollamapilo config set model llama3.2
# Or a cloud providerpilo config set provider openaipilo config set openai_api_key sk-your-keyWhichever you choose receives the task text and the page content Pilo reads. With Ollama that stays on your machine. With a cloud provider it goes to that provider under your own agreement with them, not Tabstack’s.
Running tasks
Section titled “Running tasks”# With a starting URLpilo run "find flight deals to Paris" --url https://booking.com/
# With data for form fillingpilo run "submit contact form" --url https://company.com/contact --data '{ "name": "John Doe", "email": "john@example.com", "message": "Hello world"}'
# With constraints on what it may dopilo run "research product prices" --guardrails "only browse, don't buy anything"--data, --url, and --guardrails map onto the same concepts as the hosted endpoint’s data, url, and guardrails parameters.
As a library
Section titled “As a library”import { WebAgent, PlaywrightBrowser } from "@tabstack/pilo";import { openai } from "@ai-sdk/openai";
const browser = new PlaywrightBrowser({ headless: false });const provider = openai("gpt-4.1");
const agent = new WebAgent(browser, { provider, vision: true, // full-page screenshots, for layout the DOM does not explain guardrails: "Do not make purchases",});
try { const result = await agent.execute("find flights to Tokyo", { startingUrl: "https://airline.com", }); console.log("Success:", result.success);} finally { await agent.close();}For library use with Playwright, install the browser drivers once: npx playwright install.
The action firewall
Section titled “The action firewall”Pilo treats every web page as untrusted input. By default an action firewall stops the agent from filling freeform fields (textareas, contact-info inputs, password fields) and from submitting any form containing agent-filled values the user did not explicitly approve. This is the structural defense against prompt injection, where page content tries to talk the agent into exfiltrating data through a form.
Two caller-supplied controls relax it. Both are off by default, and enabling either weakens the firewall’s data-protection guarantees.
trusted_hostnames
Section titled “trusted_hostnames”A list of hostnames where the firewall is bypassed for fills and submissions. The bypass applies only when the current page hostname and every form-action hostname (the form’s action plus any submitter formaction override) are all in the list.
pilo config set trusted_hostnames example.com,app.example.comunsafe_mode
Section titled “unsafe_mode”A global firewall disable. Neither the fill gate nor the submit gate applies, regardless of page or form-action hostname.
pilo config set unsafe_mode trueWhen a block fires
Section titled “When a block fires”If the firewall blocks a fill or submission and the agent is not running interactively, the CLI prints the three ways to enable the workflow: add the hostnames to trusted_hostnames, run interactively so the agent can request per-field approval, or enable unsafe_mode.
That footer is shown only to you. The model driving the agent never sees it, so prompt-injected page content cannot use it to ask you to disable your own protections.
Browsers
Section titled “Browsers”Pilo works with Firefox, Chrome, Safari, and Edge, and bundles a browser extension for interactive in-browser automation.
pilo extension install chrome # prints manual load instructionspilo extension install firefox # launches Firefox with the extension loadedChrome stable ignores --load-extension when launched programmatically, which is why the Chrome path is manual: enable Developer mode at chrome://extensions, choose Load unpacked, and select the directory the command prints.
Pilo can also speak WebDriver BiDi directly over a WebSocket, with no Playwright in the chain. This is experimental.
firefox --remote-debugging-port 9222 --headless --no-remote --profile "$(mktemp -d)"pilo run --browser bidi --bidi-url "ws://127.0.0.1:9222/session" "what's the weather in Tokyo?"Using both
Section titled “Using both”Pilo can call the Tabstack API for the parts a browser is bad at. Extracting clean text or matching JSON from a URL is one call rather than a navigation sequence, and PDFs are the clearest case: browsers cannot read them directly, and /extract/markdown can.
A reasonable split: Pilo for interaction and anything needing your own browser or model, /extract for reading pages, /research for questions.
Next steps
Section titled “Next steps”- Automate Tasks: the hosted endpoint, its events, and its parameters.
- Interactive Mode: how the hosted endpoint pauses for form values.
- API Reference:
/automate, the hosted endpoint Pilo is the alternative to. - Pilo on GitHub: source, issues, and the
#tabstackchannel on the Mozilla AI Discord.