---
title: Pilo: self-hosted automation | Tabstack
description: 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`](/guides/how-to-use-automate-endpoint/index.md) 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](https://github.com/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

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

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?"
```

## 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](https://platform.openai.com/api-keys) |
| OpenRouter           | Key from [openrouter.ai](https://openrouter.ai/keys)                 |
| Google Generative AI | Key from [ai.google.dev](https://ai.google.dev/)                     |
| Vertex AI            | Google 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.

## Running tasks

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.

### 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

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`

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
```

On listed hosts, prompt injection from page content can drive the agent to fill and submit any field, including personal data and credentials. Use this only for sites you fully trust to receive your data.

### `unsafe_mode`

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
```

With `unsafe_mode` enabled, prompt injection from page content can cause the agent to submit your data, including credentials, personal information, and conversation context, to attacker-controlled forms. Only enable this in trusted, controlled environments.

### 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

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](https://w3c.github.io/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?"
```

## 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`](/guides/how-to-use-markdown-endpoint/index.md) can.

A reasonable split: Pilo for interaction and anything needing your own browser or model, [`/extract`](/guides/how-to-extract-json/index.md) for reading pages, [`/research`](/guides/research/index.md) for questions.

## Next steps

- [Automate Tasks](/guides/how-to-use-automate-endpoint/index.md): the hosted endpoint, its events, and its parameters.
- [Interactive Mode](/guides/interactive-mode/index.md): how the hosted endpoint pauses for form values.
- API Reference: [`/automate`](/api/resources/agent/methods/automate/index.md), the hosted endpoint Pilo is the alternative to.
- [Pilo on GitHub](https://github.com/mozilla/pilo): source, issues, and the `#tabstack` channel on the [Mozilla AI Discord](https://discord.gg/mozillaai).
