Task orchestration you own.
Correct, sandboxed, extensible. No cloud, no account, no paywall.
vznjs.github.io/vx
Task orchestration with a cache you can trust, on a core you can extend. Each task is one shell command with declared inputs and outputs; vx runs them in dependency order and replays results from a content-addressed cache.
npm install -D @vzn/vx # or: pnpm add -D -w · yarn add -D (-W on Yarn 1) · bun add -dA prebuilt binary for Linux (glibc, not Alpine's musl) and macOS, x64 and arm64; no Bun needed. The release binary needs no Node either. Windows: run vx inside WSL2.
In a git repo with a package.json workspace:
npm install -D @vzn/vx # the scope matters: npm's `vx` is an unrelated package
npx @vzn/vx init # beside turbo.json or nx.json: migrates them; else a vx.config.ts per package
npx @vzn/vx run build --all # builds in dependency order and stores the results
npx @vzn/vx run build --all # nothing changed: every task up-to-dateBun workspaces: bun add -d @vzn/vx and bunx @vzn/vx (npm refuses to run in
a repo whose devEngines names Bun).
Without Turbo or Nx, tasks run uncached until you declare what they read (below).
init prints what it wrote and anything left to decide (a TODO). A miss you did not expect?
npx @vzn/vx why <task> names the input that moved.
init writes one task per package script, uncached. Cache one by saying what it reads and writes:
// packages/app/vx.config.ts
import { defineProject } from '@vzn/vx/config'
export default defineProject({
tasks: {
build: {
exec: { command: 'tsc -b' },
dependsOn: ['^build'], // dependencies build first
inputs: { files: ['src/**', 'tsconfig.json'] },
cache: {
outputs: { files: ['dist/**'] },
},
},
},
})npx @vzn/vx run build --all # builds, in dependency order; stores the results
npx @vzn/vx run build --all # nothing changed: up-to-date, in milliseconds
rm -rf packages/*/dist
npx @vzn/vx run build --all # restored from the cache, not rebuiltinit also writes .vx/projects.lock.json, every project's config as it evaluated.
Commit it. Every run reads its configs from the lock, so run
npx @vzn/vx lock and commit the lock again after you:
- edit any
vx.config.tsor.vx/workspace.ts, - update dependencies or plugins,
- change a file a config imports.
Otherwise runs keep the old configs. On CI, vx lock --check fails a
stale lock before any task runs.
Starters: examples/basic, and
examples/ci with a GitHub Actions workflow; CI runs both
on every commit. The CI guide
explains the workflow.
More: Quickstart ·
Configure ·
CLI · every config field ·
playground (the planner, in your browser).
- A cache you can trust: TypeScript configs are evaluated before hashing, so imports and computed values are in the key. Outputs are wiped before every run and restore: no stale file survives.
- Sandboxed tasks: opt in per task; a workspace file the task did not declare is out of its reach, so it cannot poison the cache.
- Clean exits: Ctrl-C reaps each task's process group.
- Plugins at every stage (15 hooks,
commandsincluded): remote cache, remote execution, telemetry, CLI verbs. Core ships none and needs none. - Free: MIT. No paywall, no cloud, no account.
What vx can do, not what it is limited to. Declared in .vx/workspace.ts; without any, vx runs and caches locally.
| Package | What it adds |
|---|---|
@vzn/vx-migrate |
Migrate from Turbo or Nx: write native configs, keep its remote 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-ci |
GitHub Actions job summary and Checks API |
@vzn/vx-schedule-history |
Schedule by the critical path learned from past runs |
Write your own: plugin guide · architecture.
Beside turbo.json or nx.json, npx @vzn/vx init runs @vzn/vx-migrate:
it writes a vx.config.ts per package from turbo.json or the Nx graph,
and you keep those and delete the old config. vx init --keep writes only
a .vx/workspace.ts that maps the old config instead, and its lock: a temporary start,
not a way to run the repo. The benchmark below uses native vx config.
Migration guide.
| vx | Turborepo | Nx | |
|---|---|---|---|
| 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 | No source reads (git index OIDs) | git OIDs | re-hash / daemon |
| Daemon | None | None for turbo run |
On by default |
| Per-task sandbox | Yes: kernel-level, opt-in, local | No | On Nx Cloud only |
| Plugin API | Yes: every pipeline stage | No | Yes (TS-tied) |
| Install | Single binary; no Bun, Node optional | npm + Node | npm + Node |
Every gap, and when another tool is the better pick: comparison · vx, Turborepo, Nx, Bazel.
- Nothing boots per task: no daemon, no Node process; vx spawns your command.
- No source-file reads to hash: on a clean tree, keys come from git's index; each project's
package.jsonand the lockfile are still read. - One pass: one git walk per run; each stat and lookup happens once.
| Time added over the ideal run, 1,601 projects, 9,603 tasks | vx | Turborepo | Nx | Vite Task |
|---|---|---|---|---|
| Nothing changed | 856 ms | 855 ms | 23.22 s | 11.24 s |
| Nothing changed, outputs restored | 1.45 s | 1.50 s | 21.64 s | 11.21 s |
| Cold build | 7.73 s | 10.32 s | 3 min 23 s | 9.44 s |
| Cold build: CPU the runner burns | 36.42 s | 1 min 3 s | 5 min 35 s | 25.64 s |
Time each tool adds over the ideal run; lower is better.
Benchmark workload: a synthetic monorepo of 1,601 projects and 9,603 tasks in 30 dependency levels, five core libraries a quarter of the projects use; build 300 ms, test and typecheck 150 ms, lint 75 ms, publish 30 ms; real repos with uneven task times will differ. Run 2026-10-12 on linux x64, 4 cores: vx at commit 126d60a93, Turborepo 2.11.7, Nx 23.3.0, Vite Task (vite-plus) 1.1.0.
Same graph, commands and concurrency: how it is measured.
Pre-alpha, dogfooded: this repo lints, tests and builds itself with vx. The schema may still change; 1.0's promise: versioning-1.0.md.
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; no package.json
scripts. Start with CONTRIBUTING.md and
STATUS.md.
vx is MIT licensed. The prebuilt binaries embed Bun and a few npm packages under permissive licenses; their notices ship as THIRD_PARTY_NOTICES.txt in each platform package and GitHub release.