A fast, correct task runner and build cache for JavaScript monorepos.
One binary. No daemon, no cloud, no account.
Same graph, same commands, same concurrency. Methodology and how to reproduce it.
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.
Measured on eleven real Turborepo and Nx repos too: tried on real repos.
npm install -D @vzn/vx # or: pnpm add -D · yarn add -D · bun add -dThe package ships a prebuilt binary for Linux and macOS, x64 and arm64 (Windows via WSL); no Bun needed. The binary alone, from a release, needs no Node either.
In any git repo with a package.json workspace:
npx vx init # a vx.config.ts per package, from its scriptsGive a task a cache block (what it reads, what it writes):
// packages/app/vx.config.ts
import { defineProject } from '@vzn/vx'
export default defineProject({
tasks: {
build: {
exec: { command: 'tsc -b' },
dependsOn: ['^build'], // dependencies build first
cache: {
inputs: { files: ['src/**', 'tsconfig.json'] },
outputs: { files: ['dist/**'] },
},
},
},
})npx vx run build --all # builds, in dependency order; stores the results
npx vx run build --all # nothing changed: up-to-date, in milliseconds
rm -rf packages/*/dist
npx vx run build --all # restored from the cache, not rebuiltOr copy examples/basic, a two-package starter CI
runs on every commit. More: Quickstart ·
Configure ·
CLI · every config field ·
playground (the planner, in your browser).
Keep your turbo.json, Nx project graph or .moon/. Two lines run it
under vx, with nothing rewritten:
// vx.workspace.ts
import { defineWorkspace } from '@vzn/vx'
import { turbo } from '@vzn/vx-migrate' // or: nx, moon
export default defineWorkspace({ plugins: [turbo()] })When you want configs of your own, one command writes them:
bunx @vzn/vx-migrate # vx.config.ts per package, from turbo.json, an Nx graph or .moon/The written configs derive the same cache keys, so what turbo() cached
still hits: examples/turbo runs both steps on every
commit.
turboCache() and nxCache() keep the remote cache you have (Turbo's
/v8/artifacts API, Vercel's included, or Nx's /v1/cache). What each
Turbo and Nx feature is in vx, pinned by a test:
parity map ·
migration guide.
The plugin packages (
@vzn/vx-migrateand the rest below) are not on npm yet; only@vzn/vxis.
- A cache you can trust. Configs are TypeScript, evaluated before hashing, so imports and computed values are part of the key. Declared outputs are wiped before every run and every restore: no stale file survives. On a clean tree keys come from git's index, with no file read.
- Fast where it counts. Milliseconds of overhead per package, no daemon, one git walk per run. Every optimization is recorded with the invariant that keeps it valid (ledger).
- Sandboxed tasks. Opt in per task: a read the task did not declare fails the build instead of poisoning the cache.
- Clean exits. Ctrl-C reaps every child. No orphaned dev servers.
- Plugins at every stage. Remote cache, remote execution, telemetry and new CLI verbs are plugins on documented seams, the way Vite does it. Core ships none and needs none.
- Free. MIT. No paywall, no cloud, no account.
Eleven public monorepos, each run with its own turbo.json or Nx graph
and nothing rewritten, against the tool it ships with. Both tools on the same machine and
tasks per row, medians; vx first, theirs second. Where the other tool
wins, the row says so.
| Repo | Against | Cold build | Restore outputs | Nothing to do |
|---|---|---|---|---|
| solidjs/solid | Turbo | 40.6 s / 45.5 s | 66 ms / 127 ms | 51 ms / 95 ms |
| withastro/astro | Turbo | 48.0 s / 57.9 s | 478 ms / 1.43 s | 249 ms / 1.38 s |
| payloadcms/payload | Turbo | 126.9 s / 127.5 s | 3.44 s / 3.46 s | 256 ms / 237 ms |
| medusajs/medusa | Turbo | 308 s / 315 s | 3.86 s / 7.16 s | 947 ms / 3.29 s |
| n8n-io/n8n | Turbo | 137.2 s / 133.7 s | 8.27 s / 12.5 s | 842 ms / 1.36 s |
| calcom/cal.com | Turbo | 245.7 s / 250.7 s | 17.4 s / 19.9 s | 14.7 s / 18.5 s |
| TanStack/query | Nx | 47.4 s / 55.1 s | 656 ms / 1.98 s | 174 ms / 1.88 s |
| strapi/strapi | Nx | 238.2 s / 245.7 s | 1.61 s / 3.94 s | 341 ms / 4.02 s |
| novuhq/novu | Nx | 291.2 s / 299.4 s | 3.10 s / 9.05 s | 655 ms / 8.55 s |
| TanStack/router | Nx | 199.9 s / 226.7 s | 1.00 s / 3.33 s | 505 ms / 3.21 s |
| refinedev/refine | Nx | 96.2 s / 105.9 s | 1.06 s / 1.19 s | 303 ms / 1.16 s |
Bold marks the other tool winning. Versions, revisions, scopes and harness per repo: benchmarks (astro and refine as re-measured on 2026-09-27, the rest 2026-09-10 and -11).
| vx | Turborepo | Nx | moon² | |
|---|---|---|---|---|
| Fully cached, 1,090 pkgs¹ | 510 ms | 760 ms | 3.59 s | not measured |
| Config | TypeScript, evaluated into the cache key | JSON (static) | JSON (static) | YAML, Pkl and others (static) |
| Output ownership | Strict: wiped before exec and restore | Additive (stale files survive) | Additive | Glob outputs pruned on restore |
| Clean-tree hashing | Zero reads (git index OIDs) | git OIDs | re-hash / daemon | git hash-object |
| Daemon required for speed | No | Optional | Yes | Optional (experimental) |
| Per-task sandbox | Yes: kernel-level, opt-in | No | No | No |
| Plugin API | Yes: every pipeline stage | No | Yes (TS-tied) | WASM toolchains, extensions |
| OTel CI/CD spans | Yes: otel() plugin, no OTel SDK |
No | Paid | No |
| Install | Single binary; no Bun, Node optional | npm + Node | npm + Node | Single binary |
¹ Warm, nothing to rebuild, the graph charted above. Every gap, and when another tool is the better pick: comparison · vx, Turborepo, Nx, Bazel.
² From moon's own docs (2026-09-28): config,
outputs,
hashing,
daemon,
plugins,
install;
they describe no per-task sandbox and no OpenTelemetry export. Already on
moon: moon() runs it unchanged.
Declared in vx.workspace.ts. Without one, vx runs and caches on this
machine.
| Package | What it adds |
|---|---|
@vzn/vx-migrate |
Run a Turbo, Nx or moon repo unchanged; write configs; keep a Turbo or Nx cache |
@vzn/vx-reapi |
Remote cache and remote execution over Bazel's REAPI (NativeLink, BuildBuddy) |
@vzn/vx-lockfile |
Key each project on its own lockfile slice: pnpm, bun, npm, yarn |
@vzn/vx-otel |
OpenTelemetry traces, metrics and logs, no SDK |
@vzn/vx-github |
GitHub Actions job summary and Checks API |
@vzn/vx-mcp |
vx mcp: a read-only MCP server for AI coding agents |
@vzn/vx-schedule-history |
Schedule by the critical path learned from past runs |
Write your own: plugin guide · architecture.
Pre-alpha, dogfooded: this repo lints, tests and builds itself with vx. Over 4,000 core tests plus the package suites, CI green on every commit. The schema may still change; what 1.0 will promise is in versioning-1.0.md.
| Surface | Maturity |
|---|---|
| Core task runner + caching | production-ready |
Plugin pipeline (13 hooks, commands included) |
shippable |
vx init / @vzn/vx-migrate (scripts; Turbo, Nx) |
shippable |
| First-party plugins (table above) | shippable |
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 packageNeeds Bun ≥ 1.4. Every dev task is a vx task; there are no
package.json scripts. Start with CONTRIBUTING.md
and STATUS.md, the living handoff.