Skip to content
Start free
Capabilities
Automate

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.

Hosted /automatePilo
Where the browser runsTabstack infrastructureYour machine
Which model drives itManaged, inside the callYours, including local models through Ollama
Site scopePublic websites, cannot log inWhatever the browser you control can reach
What you operateAn API callNode runtime, browser, provider key, config
InterfaceSDK, CLI, HTTPCLI and JavaScript library
Streamed eventsYes, typed SSEEvent emitter plus result object
LicenseHosted serviceApache-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.

Requires Node.js 22 or newer.

Terminal window
npm install -g @tabstack/pilo

Or run it without installing:

Terminal window
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).

Terminal window
pilo config init

Run a task:

Terminal window
pilo run "what's the weather in Tokyo?"

Pilo does not ship a model. You point it at one:

ProviderNotes
OllamaLocal models. Requires Ollama running locally.
OpenAIKey from platform.openai.com
OpenRouterKey from openrouter.ai
Google Generative AIKey from ai.google.dev
Vertex AIGoogle Cloud, needs project setup and authentication
Terminal window
# Local model through Ollama
pilo config set provider ollama
pilo config set model llama3.2
# Or a cloud provider
pilo config set provider openai
pilo config set openai_api_key sk-your-key

Whichever 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.

Terminal window
# With a starting URL
pilo run "find flight deals to Paris" --url https://booking.com/
# With data for form filling
pilo 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 do
pilo 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.

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.

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.

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.

Terminal window
pilo config set trusted_hostnames example.com,app.example.com

A global firewall disable. Neither the fill gate nor the submit gate applies, regardless of page or form-action hostname.

Terminal window
pilo config set unsafe_mode true

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.

Pilo works with Firefox, Chrome, Safari, and Edge, and bundles a browser extension for interactive in-browser automation.

Terminal window
pilo extension install chrome # prints manual load instructions
pilo extension install firefox # launches Firefox with the extension loaded

Chrome 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.

Terminal window
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?"

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.