Navvi
This server provides browser automation and persona management for Navvi, enabling interactive web tasks, scraping, and persistent identity/context management.
Manage browser personas (create, get, update, list, delete) with persistent profiles, locale, timezone, stealth, and purpose.
Control a real browser via OS-level input: open URLs, click at coordinates, fill fields, press keys, drag, mouse events, scroll, and take screenshots.
Use
navvi_browsefor high-level natural-language browsing instructions that handle multi-step flows, cookie banners, login detection, and CAPTCHAs (escalating to VNC when needed).Manage browser tabs (list, new, switch, close) within a persona.
Store and retrieve credentials securely via gopass: list, get non-secret fields, generate new passwords (which never leave the container), import from JSON, and autofill login forms.
Track persona milestones (actions, signups, posts) with evidence, tags, screenshots, and export to markdown timelines or persona briefs.
Maintain persistent knowledge context per persona: add, list, search, update, remove, and digest entries with tags.
Manage reusable flow recipes (domain/action) that are automatically loaded by
navvi_browseto fast-track or guide execution.Record browser sessions to video (MP4) or optimized GIFs.
Log into services automatically using stored credentials, with 2FA handled via a VNC URL for human intervention.
Allows searching DuckDuckGo via browser automation, with persistent sessions and anti-detection.
Supports interacting with GitHub, including starring repositories, searching, and attaching screenshots to pull requests.
Enables logging into Tutanota (Tuta) with stored credentials, handling forms and CAPTCHAs automatically.
Navvi uses a model to choose among controls and fields found by code, then saves a scraper with selectors, fingerprints and a navigation trace. Healthy repeat runs reuse the prompt interpretation and scraper without model calls. When a field or step changes, Navvi can ask the model for a targeted repair; some changes still require a person or recompilation.
Jev supplies fast typed decisions. Navvi adds persistence, structured extraction, replay and repair around those decisions. Jev also uses a text-capable fallback for prompt interpretation and values to type; those calls are included in usage.
See it work
1. Compile: Haiku versus Jev
Same task, separate empty caches, Haiku on the left and Jev on the right. The real-site comparison is pending owner review. Remote OK served unrelated roles on its Python-filter page, so that attempt is not a valid speed comparison. A Hacker News search comparison is being reviewed as an alternative.
2. Reuse: the saved scraper

Watch the video: ten records from a real Remote OK search, then the same prompt in a fresh browser with zero model calls. The two sequential runs are aligned, with original clocks (42.6 s / 3.7 s). This historical capture demonstrates reuse, but its result relevance did not pass the launch review. It is not an accepted Python-job search demo. Browser execution still has a cost.
The controlled fixture below demonstrates compile, deliberately changed markup,
and replay. Its answers are recorded fixtures: it shows behavior, not live model
latency or a production-site guarantee. Video, npm run demo.

The older search-form comparison is a fixture recording, not
Remote OK. Current capture instructions and the two-run real-site recorder are in
docs/recording.md. Failed recordings retain evidence and do
not export a success clip.
The historical measurement table and Jev question-bank hillclimb report a tuned scenario set. Their cell counts are harness checks, not independent semantic accuracy. They do not establish universal speedups, unseen-site accuracy or current prompt-to-output costs. A new live demo must retain its own rows, timings and revision.
Remote OK capture provenance · Separate source verification.
Related MCP server: puppeteer-real-browser-mcp-server
Install and run
Node 22+. The published compiler is 3.0.0. The separate --decider/--writer
controls and highlighted-text extraction fix on main are newer than that release;
use the source installation below to reproduce the proposed Hacker News demo.
Install the published compiler with Chromium:
NAVVI_BROWSER=chromium npm install -g navvi@3.0.0
npx playwright@1.60.0 install chromium
navvi "Search Remote OK for Python jobs and extract up to 10 results with job title, company, location and job link. Exclude ads." https://remoteok.com/ --browser chromium --max-pages 1 --max-items 10 --out jobs.json
# Repeat the identical command to reuse the saved prompt interpretation and scraper.For the default Camoufox browser, use npm install -g navvi@3.0.0 without
NAVVI_BROWSER; postinstall downloads Camoufox. Version 3 replaces the earlier
2.x product with the scraper compiler.
To work from source:
git clone https://github.com/fellowship-dev/navvi.git
cd navvi
NAVVI_SKIP_BROWSER_DOWNLOAD=1 npm ci
npx playwright install chromium
npm run build
node dist/bin/cli.js --helpNode 22+. Select a chooser below; an installed, signed-in Claude Code or Codex CLI
can answer on your subscription. Jev needs a TypeSafe or AI Gateway key plus a
text-capable fallback. A Gateway account must have access to the configured text
model as well as Jev; access to Jev alone does not cover prompt interpretation
or typed text. Browser time and subscription/API charges still apply.
Agents: read SKILL.md; llms.txt indexes the docs.
What you get
JSON or CSV records with a
_sourceURL.A reusable scraper at
storage/key_value_stores/scraper-cache/<cacheKey>.json. The rest ofstorage/can contain live browser sessions; keep it private.Cached prompt interpretation and healthy scraper replay with zero model calls.
--force-recompiledeliberately bypasses reuse.A targeted repair path plus explicit failure statuses when automation cannot finish.
A stderr summary with pages, items, chooser usage and healing events.
Typed values when you ask for them:
--fields name,price:money,stock:boolean(ortypeon a field in the JSON input) coerces the extracted text after the fingerprint check:moneyandnumberread$ 6.990as6990,12.990,50as12990.5and12 990as12990,integertakes a whole number,booleanmaps stock phrases in Spanish and English (En stock,Agotado,Out of stock,Disponible: No),urlresolves to an absolute http(s) URL; a value that does not coerce isnull. A bare1.250is 1250 undermoneyandinteger, where a three-decimal reading is impossible, andnullundernumber, where it is an ordinary weight and nothing in the text settles it. The type is recorded in the compiled scraper, so a replay coerces the same way with no model call. Untyped fields stay strings.A URL list as the start:
--from-url <url>(or a{ "requestsFromUrl": "<url>" }entry instartUrls) fetches a URL that answers the pages to scrape as newline text or JSON (an array of URLs or of{ url }objects, or an object whoseurls,dataoritemsis one), so a backend endpoint can feed the daily target list directly.
Choosers
Navvi never lets a model write a selector or a script. It enumerates the
candidates itself and asks a chooser to pick. A run picks two of them,
independently: the decider (--decider) answers the structured questions —
which of these candidates, yes or no, score this — and the writer
(--writer) answers the occasional free-text one, the search query to type
into a box. Five backends, one contract, and only Jev is limited to one role:
name | Who answers | Needs | Decider | Writer |
| Claude Code ( |
| yes | yes |
| Codex ( |
| yes | yes |
| Jev by TypeSafe, through Vercel AI Gateway or direct |
| yes | no |
| Any AI SDK model |
| yes | yes |
| The coding agent running the command, over stdio | Nothing | yes | yes |
Without --decider the order is: a key (jev, then model); else Claude
Code, then Codex, when installed and signed in (an installed CLI that is not
signed in never wins; the run says so and names claude or codex login);
else agent. The choice and its reason print on stderr unless --quiet.
NAVVI_CLAUDE_MODEL (default haiku) and NAVVI_CODEX_MODEL pick the CLI
model. CLI usage reports $0.0000 with billing subscription; Claude Code's
own cost figure is kept as reportedCostUsd in the usage.
Without --writer the decider writes its own text — every backend but Jev
can. Jev answers choices, booleans and scores but cannot write, so its text
questions go to a second backend, and there the order is the other way round,
subscription before metering: claude, then codex when on PATH, and only
then a metered API key (ANTHROPIC_API_KEY first, since a dedicated key is a
deliberate choice, then AI_GATEWAY_API_KEY). So AI_GATEWAY_API_KEY routes
Jev's structured questions over Vercel, which is free for Jev, while the text
questions still prefer a signed-in CLI on your subscription; the Gateway text
model is used only when no CLI is installed. Sign-in cannot be checked without
running the CLI, so an installed but signed-out CLI is tried once, says so, and
the run moves to the next backend instead of failing. An explicit --writer model (or --chooser model) always means the API model, whatever is installed.
--decider-transport gateway|typesafe says which API Jev is reached over.
Unset, it is inferred from the keys, the Gateway first when both are present;
--decider-transport typesafe forces the official TypeSafe API
(api.typesafe.ai) even with AI_GATEWAY_API_KEY in the environment, and
refuses the run rather than routing the other way when the matching key is
missing.
--chooser is the one flag those three replace, and it keeps working exactly
as before: it names the decider and leaves the writer derived. --decider and
--writer win over it, so --chooser jev --decider claude runs Claude Code.
The stderr summary reports the run under chooser, plus a writer line naming
the second source and its share of the tokens and cost when one answered the
text, so a run is attributable per role.
A locally-run open-source model is a designed seam, not a shipped backend.
Either role would take an OpenAI-compatible base URL plus a model id — the two
values llama.cpp, Ollama, vLLM and LM Studio all expose — as a local writer
and a third --decider-transport. Nothing accepts local today: it is
documented rather than enumerated, so no flag can select a backend that would
throw mid-run. The interface it must satisfy is Chooser in
src/chooser/chooser.ts (name, ask(batch), usage()); the comment above
textFallbackFor in src/chooser/index.ts spells out the rest.
With the agent chooser, the CLI prints each question batch on stdout between
---NAVVI-QUESTIONS--- and ---END--- and reads one JSON answer line from
stdin. When stdin cannot stay open, --agent-mode file parks the batch in
storage/questions/<token>.json, exits 3, and
--answers answers.json --resume <token> continues. Full protocol in
SKILL.md.
How it differs
The reusable scraper is the product. A model-backed browser interaction produces an artifact that code can execute again, with fingerprints and alternatives to help detect and repair changes. This is useful for recurring extraction rather than asking an agent to rediscover the same workflow every time.
The side-by-side recorder compares Navvi + Jev with Navvi + Haiku under the same task. It does not measure Navvi against Jev Ultra Fast as a separate product. A healthy replay recording also does not prove live healing; demonstrate drift separately before making that claim.
Local and Apify
Locally Navvi runs through the CLI: Camoufox by default, Chromium with
--browser chromium, profiles and compiled scrapers under --storage
(default ./storage).
On Apify the same code is the actor under .actor/: the manifest, the input
schema (the CLI flags as fields, with section captions and descriptions
written for a model reading them through Apify's MCP server), the dataset
schema and two Dockerfiles. Dockerfile is the default build on
apify/actor-node-playwright-chrome; Dockerfile.camoufox builds on
apify/actor-node-playwright-camoufox and is the switch for a site that
challenges Chromium. Both image tags carry the Playwright version and must
equal the playwright pin in package.json; node scripts/check-image-pins.mjs
fails CI when they disagree. CI pushes every green main to the beta build
tag through apify/push-actor-action when the APIFY_TOKEN repository
secret is present; latest is a manual promote. node scripts/push-beta.mjs
pushes the Chromium beta from a signed-in CLI and --camoufox pushes the
Camoufox build under the beta-camoufox tag of the same version.
The actor input differs from the CLI in three places: startUrls takes
{ url } and { requestsFromUrl } entries (Apify's request-list editor);
profile is store only, chooser and decider are jev or model and
writer is model (the image has no CLI to run on a subscription); a caller key
comes as a secret input (typesafeApiKey, gatewayApiKey,
anthropicApiKey) and is used for that run only. scriptId pins a compiled
scraper by key, with scraperStore naming the key-value store when the key
is bare (default scraper-cache in your account). Locally,
npx apify run --input-file input.json # runs dist/src/main.js with local storageruns the actor entry with the same input.
Proxies and residential IPs
Yes: everything Apify Proxy offers is selectable in the Console's proxy editor
and reaches Crawlee. The proxy input is Apify's own proxy object, so the
groups and the country you pick survive validation and are handed to
Actor.createProxyConfiguration, which Crawlee then rotates per session for
every browser it launches.
{
"proxy": {
"useApifyProxy": true,
"apifyProxyGroups": ["RESIDENTIAL"],
"apifyProxyCountry": "CL"
}
}In the Console: open Proxy, choose Apify Proxy, then select the
RESIDENTIAL group and, optionally, a country (two-letter code, e.g. US,
CL). Leaving the groups empty lets Apify pick datacenter proxies
automatically.
Residential costs money, by the gigabyte, billed to your Apify account on top of the actor's events — datacenter proxies are the cheap default and are included in most plans. Turn residential on for the sites that need it, not for every run.
It is the fix for
blocked_bot_detection. A run that ends there is usually being blocked on the IP, not on the page: re-run it withapifyProxyGroups: ["RESIDENTIAL"], and on a stubborn site with the Camoufox build as well.Residential is not on every plan. If your account has no access to the group you selected, the platform run stops before the browser opens with a configuration error that names the group and the country it asked for, so the fix is legible instead of a stream of 407s. Locally, where the Apify SDK only warns, the run says on stderr that no proxy was created and continues without one.
Your own proxies go in
proxyUrlsinstead, and are rotated the same way. Apify Proxy and your own proxies are one choice, not two layers: asking for both is refused at validation (Apify's ownProxyConfigurationalso refuses to combine them) rather than one silently shadowing the other.
Pay-per-event
On Apify the actor charges four events, priced in the Apify Console, never in
code. Every run ends with a SUMMARY record in the run's key-value store
carrying the status, counts, chooser usage, healing events, the scriptId to
pin next time, the charged event counts and the zero-data-retention state.
Event | Charged |
| Once, first thing; covers navigation model spend when the operator key is used |
| Once per template, the first time a page passes the fingerprint check with a scraper compiled this run; a cache hit charges nothing |
| Per scraped page (listing, paginated page, detail page); the limit is checked before every page |
| Per dataset item |
When the run's charge limit is reached the items pushed so far stay in the
dataset and the run ends charge_limit. Off the platform nothing is charged
and every count in the summary is zero; a local run with
ACTOR_TEST_PAY_PER_EVENT=1 ACTOR_USE_CHARGING_LOG_DATASET=1 charges at $1
per event against ACTOR_MAX_TOTAL_CHARGE_USD and writes the charging log to
the charging_log dataset instead.
Who pays what under pay-per-event, per Apify's pricing docs: the caller pays the events; the actor's platform usage (compute, residential proxy, storage) is the operator's cost, which is why the event prices carry a compute margin. The first platform run under this pricing confirms the split and this paragraph is updated with the observed numbers.
Exit codes
Exit | Status | Meaning |
0 |
| Records written |
1 |
| The run stopped short; stderr says why |
2 | configuration or validation error | Bad flags, missing key (the message names |
3 |
| Questions parked in |
4 |
| Retry later, raise the cap, or switch chooser |
Limits
Models can mistake a filled form for a completed search. Validate the target results and output meaning, not just status or non-empty fields.
Repairs supported field/step changes, but cannot guarantee repair of a redesign. An empty listing may report
no_items_found; it is not always distinguishable from a changed item selector. Missing compiled rows get a bounded five-second wait.Extraction reflects the source. A selected search filter does not guarantee every returned job matches its meaning; verify relevance separately.
No captcha solving. On a headed run (
--headed) a challenge is handed to the person at the keyboard and their step is recorded; unattended runs reportblocked_bot_detection— see Proxies and residential IPs for the usual fix.Logins use
--profile local. Secrets come fromNAVVI_SECRET_<NAME>(--secret name), a--secrets-file, or a hidden TTY prompt; never the command line, never a question, a log or the scraper JSON.The run stays on the start URLs' domains unless
--allow-domainwidens it; private hosts need--allow-private-host; destructive-looking actions need--allow-mutation.Pagination and item caps default to 10 pages and 1000 items (
--max-pages,--max-items).
Measurements
With and without Jev on the same pages: questions, wait time, cost and heal
rate live in docs/measurements.md.
Development and tests
npm run typecheck
npm test
npm run buildCI runs the same offline suite with Chromium and recorded model answers; it
needs no model key. Live model tests are separate: npm run test:live.
Testing and CI explained · Recording guide.
Report a failing URL and a redacted summary in an issue.
Never include browser profiles, cookies or API keys.
License
MIT.
Available Tools
31 toolsTool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
31 tool updates
v3.21.0- First observed
navvi_account - First observed
navvi_atomic - First observed
navvi_browse - First observed
navvi_click - First observed
navvi_context - First observed
navvi_creds - First observed
navvi_drag - First observed
navvi_fill - First observed
navvi_find - First observed
navvi_flow - First observed
navvi_hold - First observed
navvi_list - First observed
navvi_login - First observed
navvi_milestone - First observed
navvi_mousedown - First observed
navvi_mousemove - First observed
navvi_mouseup - First observed
navvi_open - First observed
navvi_persona - First observed
navvi_press - First observed
navvi_record_gif - First observed
navvi_record_start - First observed
navvi_record_stop - First observed
navvi_screenshot - First observed
navvi_scroll - First observed
navvi_start - First observed
navvi_status - First observed
navvi_stop - First observed
navvi_tab - First observed
navvi_url - First observed
navvi_vnc
TDQS
Scored across 31 tools
Each tool has a clearly distinct purpose: high-level browsing (navvi_browse), low-level actions (navvi_click, navvi_fill), credential management (navvi_creds), persona lifecycle (navvi_persona, navvi_account, navvi_milestone), etc. Even related tools like mouse actions are differentiated by action type. No overlapping ambiguity.
All tools follow the consistent snake_case pattern 'navvi_<verb_or_noun>'. The naming is predictable: verbs for actions (browse, click, find) and nouns for entities (context, flow, persona). No mixed conventions or irregular patterns.
31 tools is higher than typical (3-15) but well-justified given the comprehensive scope: browser automation (atomic & high-level), credential management, persona profiles, knowledge context, recording, and infrastructure. Each tool has a defined role without redundancy.
The tool set covers the full lifecycle of persona-driven web automation: browser control (open, find, click, type, scroll), credential handling, knowledge storage, flow recipes, and recording. Minor gaps exist (e.g., file uploads, extension management) but do not hinder primary workflows.
Maintenance
Related MCP Connectors
Undetectable cloud browser sessions for AI agents and scrapers. Navigate, extract, click, captcha.
Stealth web automation for AI agents. Login, signup, navigate, screenshot.
Stealth web automation for AI agents. Login, signup, navigate, screenshot.
AI-powered browser automation — navigate, click, fill forms, and extract data from any website.
Related MCP Servers
- FlicenseNot gradedqualityNot gradedmaintenanceEnables AI agents to authenticate with websites using a real Chromium browser with anti-detection measures and human-in-the-loop support for captchas and 2FA. Features stealth browsing, human-like interactions, and persistent session storage to automate and resume login workflows.-
- AlicenseNot gradedqualityDmaintenanceProvides AI assistants with powerful, detection-resistant browser automation capabilities, allowing them to control a real web browser to navigate, extract content, and perform tasks while avoiding bot detection.6 npmMIT
- AlicenseAqualityCmaintenanceEnables AI agents to fully control a browser for web automation, including navigation, clicking, typing, scrolling, screenshots, and DOM inspection, with session persistence and anti-bot bypass.4027 npmMIT
- AlicenseNot gradedqualityBmaintenanceProvides a persistent browser profile for AI agents, enabling them to log in once and maintain sessions across restarts. Supports 20 tools for browsing, navigation, text extraction, and screenshot.1MIT