The fastest way to build a monorepo.
vx runs your task graph, remembers every result, and never does the same work twice.
The runner adds seconds to a cold build where others add minutes. On a 1,090-package graph of 3,270 tasks whose ideal schedule is 3m 38s, vx finishes in 3m 46s (+0:08), Turborepo in 5m 13s (+1:35) and Nx in 34m 44s (+31:06) — 8 ms of overhead per package against 88 ms and 1,712 ms, so the graph can grow and the runner stays in seconds. The cold build burns 35 s of CPU in vx, 73 s in Turborepo and 114 minutes in Nx; a fully cached run replays the graph in 510 ms.
On a real Turbo monorepo (solidjs/solid, its own turbo.json, no
config rewritten) vx replays a cached build in 51 ms where Turbo takes
95, restores every output in 66 ms against 127, and builds cold in
40.6 s against 45.5 s. Measured, reproducible, on hardware you own
(benchmarks).
One binary. No daemon, no cloud, no account, no paywall. Sandboxed tasks, a plugin at every stage, seconds of overhead at a thousand packages.
vx does one thing — run and cache a task graph, correctly — and stops there. Remote caches, remote execution, telemetry, AI agents, learned scheduling and your own verbs are plugins on documented seams, not features inside: what Nx would be if it weren't a product.
📖 Documentation site → — guides, architecture, caching, and the full CLI / config reference.
# From npm — ships the prebuilt standalone binary (no Bun required):
npm install -g @vzn/vx # or: pnpm add -g @vzn/vx · bun add -g @vzn/vx// vx.config.ts
import { defineProject } from '@vzn/vx'
export default defineProject({
tasks: {
build: {
exec: { command: 'tsc -b' },
dependsOn: ['^build'],
cache: {
inputs: { files: ['src/**'] },
outputs: { files: ['dist/**'] },
},
},
test: {
exec: { command: 'bun test' },
dependsOn: ['build'],
cache: { inputs: { files: ['src/**', 'tests/**'] }, outputs: { files: [] } },
},
ci: { dependsOn: ['lint', 'test'] }, // umbrella; runs both
},
})vx init # scaffold vx.workspace.ts + a vx.config.ts per package from scripts
vx run build # cwd project + its workspace deps
vx run test --all # every project that declares `test`
vx run ci --affected # only what changed since origin/HEAD
vx watch lint # re-run on file changes
vx run build --dry # show the plan, don't executeEvery task runner caches. vx caches correctly — and stops work others would redo:
- Config is code, and the cache knows it.
vx.config.tsis evaluated before hashing, so imports, presets, and computed values all participate in cache identity. Change a shared preset, and exactly the right tasks re-run. - Outputs are owned. Declared outputs are wiped before every execution and every restore. Your tree ends each run bit-identical to the cached snapshot — stale files cannot exist.
- Hashes come from git. On a clean tree, deriving every cache key costs zero file reads, zero stats, zero database lookups. At 15,000 files that's a 3.2× faster warm path.
Exact bitset graph algorithms for scheduling. One bulk git
enumeration per run, partitioned by binary search. Restores that
skip extraction entirely when the tree already matches. In-process
tar (no subprocess on the hot path). Atomic artifact publishes.
Single-transaction metadata writes. Every optimization is recorded
with the invariant that keeps it valid —
packages/vx/docs/optimizations.md is the ledger, and
packages/vx-bench/ reproduces the numbers.
- Corruption can't go live. A remote artifact is verified against its content digest and validated before it enters the store (zstd-bomb and oversize downloads refused); bad bytes degrade to a cache miss, never a wrong hit and never a crash.
- Clean exits. SIGINT/SIGTERM reap every child process — no orphaned dev servers in CI.
- Readiness you can bound. Persistent tasks gate downstream work
on a
readyWhensignal;exec.timeoutbounds any task (with--timeout/ workspace defaults),exec.retries+--retryabsorb flakes. - Flaky tasks are found, not guessed. The same inputs both passing
and failing is the definition, and vx holds every hash and outcome
locally: a run names them under its footer,
--summarizetypes them per task,vx infolists them. No service. - Kernel-level sandboxing, opt-in per task, that fails the build on violation instead of hiding it.
Configs are TypeScript — powerful, but a program's output can vary
with its environment. vx lock freezes the fully-resolved task graph
into a committed vx-lock.json, pnpm-style:
vx lock # evaluate everything once, write vx-lock.json
vx lock --check && vx run ci --frozen # CI: audit, then run EXACTLY that graph| Command | Evaluates configs | Uses lock |
|---|---|---|
vx run |
always, live | never — local truth has no asterisks |
vx run --frozen |
never | yes; refuses if absent or a config file changed since locking |
vx lock --check |
full graph | compares — catches env and import drift that byte hashes cannot |
Env values read at lock time are frozen by design — cache keys become
reproducible across machines. Bonus: --frozen runs skip config
evaluation entirely (~120 ms back per 1,000 packages). No other
runner has an equivalent.
TypeScript config with real imports · task graph with ^task
resolution that bridges packages without the task · multi-task runs
with one shared graph · pnpm-style filters and --affected ·
watch mode · --dry / --graph plans · persistent dev servers ·
retries, timeouts, --continue modes · per-layer cache control
(--cache=local:r,remote:) · vx why explains a re-run from the
persisted input fingerprints · vx last replays a recorded run ·
vx info,
--summarize, --profile Chrome traces, --report · vx cache prune
with TTL and size caps · bunx @vzn/vx-migrate from turbo.json or an Nx graph.
Core is the pipeline: discover projects, evaluate configs, build the
task graph, derive keys, schedule, execute, cache, observe. Plugins
declared in vx.workspace.ts hook each stage, Vite-style, on one
VxPlugin object:
| Stage | Hook | A plugin can… |
|---|---|---|
| workspace | config(ws, ctx) |
edit the workspace config before it is used |
| project | project(config, ctx) |
add, remove or rewrite a project's tasks (keyed like yours) |
| graph | graph(nodes, ctx) |
add or drop edges, mark tasks requested |
| key | key(task, ctx) |
fold extra material into the cache key (named in vx why) |
| schedule | schedule(nodes, ctx) |
decide which ready task runs first |
| execute | executor(ctx) |
decide WHERE a task's command runs (local, a REAPI worker) |
| store | cache(ctx) |
decide where artifacts live (local, a shared remote) |
| observe | telemetry(ctx) / setup |
receive every run record, or the raw event bus |
| cli | commands |
add verbs to vx |
Core applies no plugin by default and names none. Running here and
caching here are its floor — the tail of every executor list and cache
chain — so a workspace with no vx.workspace.ts runs and caches, and a
plugin that declines a task hands it back to this machine. First-party
plugins:
@vzn/vx-reapi (Bazel Remote Execution API —
remote cache and remote execution against NativeLink, BuildBuddy,
Buildbarn or bazel-remote), @vzn/vx-otel
(OpenTelemetry traces + metrics + logs, zero SDK deps),
@vzn/vx-github (Actions job summary + Checks
API), @vzn/vx-mcp (vx mcp — a read-only Model
Context Protocol server for AI coding agents, no SDK),
@vzn/vx-migrate (adoption in one package:
turbo() runs a Turbo repo under vx with nothing written, bunx @vzn/vx-migrate writes configs from turbo.json or an Nx graph, and
turboCache() / nxCache() keep a remote cache speaking Turbo's
/v8/artifacts API or Nx's self-hosted spec — the wire is theirs, the
artifacts are vx's),
@vzn/vx-lockfile (pnpm(), bun(), npm(),
yarn(): the lockfile keyed per project, so one install re-keys only the
projects it reaches, and --affected follows), and @vzn/vx-schedule-history
(order by learned critical path) and @vzn/vx-migrate
(bunx @vzn/vx-migrate: turbo.json or an Nx graph → vx.config.ts, through
core's migration seam). Core ships no plugin and reads no other runner's
format; nothing
distributed ships in this repo; the seams are how you build it.
| vx | Turborepo | Nx | |
|---|---|---|---|
| Fully cached, 100 pkgs¹ | 144 ms | 279 ms | 583+ ms |
| Config | TypeScript, evaluated into the cache key | JSON (static) | JSON (static) |
| Output ownership | Strict — wiped before exec AND restore | Additive (stale files survive) | Additive |
| Clean-tree hashing | Zero reads (git index OIDs) | git OIDs | re-hash / daemon |
| Daemon required for speed | No | Optional | Yes |
| Per-task sandbox | Yes — kernel-level, opt-in | No | No |
| Plugin API | Yes — executor / cache / telemetry seams | No | Yes (TS-tied) |
| OTel CI/CD spans | Yes — otel() plugin, zero OTel-SDK deps |
No | Paid |
| Install | Single binary — npm or 1 curl line, no Node/Bun needed | npm + Node | npm + Node |
¹ Wall-clock, direct binaries, same machine and workspace — full
methodology and more scenarios in
packages/vx/docs/benchmarks.md.
Most projects can move in an afternoon. The mapping is mechanical:
// vx.config.ts (after)
import { defineProject } from '@vzn/vx'
export default defineProject({
tasks: {
build: {
// Name the command (Turbo reads package.json scripts). The child
// env is ISOLATED: a cache-input env var must also be passed
// through, or the key would vary while the task can't see it.
exec: { command: 'tsc -b', env: { passThrough: ['NODE_ENV'] } },
dependsOn: ['^build'],
cache: {
inputs: { files: ['src/**'], env: ['NODE_ENV'] },
outputs: { files: ['dist/**'] },
},
},
},
})Differences to know:
- vx requires
exec.commandin the config — we don't readpackage.jsonscripts implicitly. - vx requires
cache.inputs.fileswhen caching is enabled (no default$TURBO_DEFAULT$). - vx defaults caching off; opt in per task by adding the
cacheblock. - Persistent tasks:
persistent: { readyWhen: 'regex' }(Turbo uses justpersistent: true). - Remote caching is a plugin, not a built-in — connect one and every
vx runreads through it.
Side-by-side feature matrix + every known gap: packages/vx/docs/comparison.md.
What a Turbo or Nx user relies on, spelled in vx and pinned by a test:
packages/vx/docs/parity.md.
bin.ts → cli/index.ts dispatches subcommands.
orchestrator/run.ts:run() calls prepareRun() which discovers the
workspace, loads configs, builds the package + task graph, opens the
cache (local SQLite + an optional remote layer), and installs plugins
from vx.workspace.ts. The two-tier scheduler runs the graph in
topological order with bounded concurrency (confirmed cache hits
restore ahead of their deps); each task hits the cache (hash → get →
restore on hit; spawn → save on miss) or short-circuits as a group /
persistent. Every observation flows through one event bus — the
terminal renderer subscribes directly, and plugins receive the
versioned telemetry contract (TelemetryRecord / RunSummaryRecord)
— that's how telemetry and cache plugins export without core knowing
them. Core never imports a plugin; the arrow only points plugin → core.
Every module has a docs page; every interface is a swappable seam.
Read packages/vx/docs/architecture.md for the module
map; the design record lives under packages/vx/docs/design/.
Full technical docs live under packages/vx/docs/ and on the
documentation site:
packages/vx/docs/architecture.md— module map + data flowpackages/vx/docs/schema.md— every config fieldpackages/vx/docs/caching.md— cache-key derivation + invalidation tablepackages/vx/docs/execution.md—vx runlifecyclepackages/vx/docs/cli.md— every flagpackages/vx/docs/comparison.md— Turbo / Nx / vite-task feature matrixpackages/vx/docs/modules/— one reference page per source module
The design record lives under packages/vx/docs/design/; the
maintainers' handoff is packages/vx/docs/STATUS.md.
Pre-alpha. The schema is settling; we bump CACHE_VERSION rather
than maintain back-compat. ~2,500 core tests plus the package suites; CI green on every commit;
the project dogfoods itself (vx run ci). Published on npm:
@vzn/vx (a prebuilt standalone
binary).
Production readiness for the core task runner: the semantics are solid; it is dogfooded continuously. Linux and macOS, x64 and arm64; Windows runs vx under WSL (POSIX shell is the API), with no native build.
| Surface | Maturity | Notes |
|---|---|---|
| Core task runner + caching | production-ready | dogfooded continuously; ~2,500 core tests + the package suites, green |
Plugin pipeline (9 hooks, commands included) |
shippable | crash-isolated, re-validated; the local executor + cache are the floor under every plugin |
vx init / @vzn/vx-migrate (scripts; Turbo, Nx) |
shippable | one config per package, TODOs where a source cannot say |
REAPI remote cache + execution (@vzn/vx-reapi) |
shippable | Bazel AC + CAS + Execute; NativeLink / BuildBuddy / Buildbarn / bazel-remote |
OTel export (@vzn/vx-otel) |
shippable | OTLP traces + metrics + logs, zero SDK deps |
GitHub Actions (@vzn/vx-github) |
shippable | job summary + Checks API run |
MCP server (@vzn/vx-mcp) |
shippable | vx mcp — read-only tools for AI agents, no SDK |
Adoption (@vzn/vx-migrate) |
shippable | turbo(): a turbo.json workspace runs with no vx.config written; turboCache() / nxCache(): any /v8/artifacts or Nx /v1/cache server; the migrate CLI |
Lockfile keys (@vzn/vx-lockfile) |
shippable | pnpm() bun() npm() yarn(): per-project dependency-closure keys; --affected follows |
git clone https://github.com/vznjs/vx && cd vx
bun install
bun packages/vx/src/bin.ts run ci --all # lint + test + docs build, every package
bun packages/vx/src/bin.ts run build --filter @vzn/vx # cross-target binaries → packages/vx/dist/vx is self-hosted: every dev task routes through bun packages/vx/src/bin.ts run <task> per each package's own vx.config.ts. No package.json scripts; CI invokes vx directly. Start with packages/vx/docs/STATUS.md — the living handoff — and CLAUDE.md.
MIT — see LICENSE.