Turborepo, Nx and Vite Task undo your commit on a cache hit
Commit a file that sits in a task’s output folder. On the next cache hit, Turborepo, Nx and Vite Task write the old copy back over it, and the run reports success.
Read the post// the vx blog
Stories, features and the design behind vx.
Commit a file that sits in a task’s output folder. On the next cache hit, Turborepo, Nx and Vite Task write the old copy back over it, and the run reports success.
Read the postA sandboxed task reads what it depends on, and nothing beside it
A sandboxed task can read the outputs of the tasks it depends on, in any project, and never their sources. With hermetic: true it reads nothing of the machine you did not name.
DTE workers that fit the work, and a footer that says how full they ran
vx DTE puts each task on a machine type that can run it, books it by its measured memory peak and CPU, stops a server once nothing needs it, and prints how busy every worker was.
One test suite, six task runners, the same 993 cases
993 task-runner cases, compiled from the test suites of Nx, Turborepo, Vite Task, moon, Nix and vx, run sandboxed against all six. Every result is on one page.
A DTE relay that stores nothing
vx DTE's relay keeps no message, no timer and no storage row. It passes frames between sockets, and the two ends make sure each one lands.
$12k a year saved with vx vs Nx Cloud
vx's CI is one run spread over its own Linux and Mac runners. On Nx Cloud's Team plan with Nx's agents, the same CI would cost about $12,192 a year. On vx it costs $0.
When running beats downloading
If a download would take longer than running the task, vx runs the task. 500 remote hits finish in 1.15 s instead of 3.12 s.
Every task has a key
A task you never cache still has a key. Change its inputs and every cached task after it runs again, so you never get a build made for old inputs.
What a run does not do
A run does no cleanup at exit, never walks the shared store, and shows no "time saved" it did not measure.
Brackets in a glob mean brackets
In a vx glob, [jt] matches one character, as in Turbo, Nx and your shell. A Next.js route folder like app/[id] needs escaping.
Cache transfers wait on the network
Cache transfers mostly wait on the network, not a core. vx runs up to 64 at once, so 1,000 uploads to a cache 30 ms away take 2.5 s.
A cache hit is a file copy
A local cache hit is a file copy the kernel does. 1,000 hits of 96 KiB restore in 411 ms, and vx never hard-links.
Branch caches a server enforces
A pull request reads what main built and can never write where main reads. The CI token decides writes, one header steers reads. How it works, and how a cache server builds its half.
vx on real repos: wall times
Seven open-source monorepos that already run on Turborepo or Nx, timed under their own runner and under vx: cold, cache restore, one-file edit and a clean clone. Raw seconds, min of three, slower rows included.
Say which projects may depend on which
Tag your projects by layer and say which may depend on which. vx lock refuses a dependency that breaks a rule, and runs pay nothing.
One name per setting
Every vx setting has one name. run.retries is VX_RUN_RETRIES and --retries. Learn one, know all three.
Where a task run spends its time
We traced one workspace through vx, Turborepo 2.11.8 and Nx 23.3.0, cold and warm. A cold run is the tasks. A warm run is how little the tool does.
An input changes mid-run: what four task tools do
A file changes while a task runs. Does the cache save the output anyway? We read the source of vx, Turborepo, Nx and Vite Task.
When an input moves mid-run, vx keeps the result and skips the save
Save a file while a build runs and vx keeps the result but caches nothing, so a later hit never brings back output built from the wrong source.
A glob stops at the next project
A project's globs stop at the next project's folder. A change elsewhere reaches a task only through a declared dependency.
A cache that stays the size you chose
vx cache clean keeps the cache at the size you pick, by age and by last use. It refuses a bound of zero.
Carry the scheduler's memory between CI runners
A fresh CI runner has no run history, so it can't know which tasks are slow. One JSON file carries the learned times from runner to runner.
A cold run asks the cache nothing
With an empty cache, vx skips the lookups that would all miss. On 1,000 projects the first task starts 136 ms after vx run.
Which task runs first?
When more tasks are ready than workers, a runner has to pick. Five rules for picking, the graphs where each one wins and loses, measured with vx, and why recorded timings beat every rule that has to guess.
How many tasks at once? Count the cores you are given
In a container, the CPU count a program sees is the host's. vx reads the limit the kernel enforces, so it never starts more tasks than you have cores.
A key with nothing to run
A task can have inputs and no command. Tests that depend on it run again when those files change, and nothing extra spawns.
The reference at your version
vx docs <query> searches the docs for the vx version you run, so the answer matches your binary.
From npm scripts or Vite Task
One command turns package.json scripts or Vite Task config into vx tasks. npm pre and post hooks keep their order.
Guard rails that tell you the fix
When vx stops something, it names the file and the fix. Sandbox refusals, two tasks writing one folder, a config that hangs.
A scheduler that learns from your runs
@vzn/vx-history starts the longest chain first and keeps memory-hungry tasks apart, using times and memory from your past runs.
The basics, done carefully
vx run picks the project you are in, never waits on a picker in a pipe, and gives each task its own fold on GitHub Actions.
An upgrade you can trust
vx upgrade checks npm’s registry signature and the SHA-512 before it replaces anything, upgrades an npm, pnpm, bun or yarn install through that package manager, and swaps back if the new binary does not start.
Extend vx in an afternoon
One command scaffolds a working plugin and its test. Bring your own remote cache through one small interface, hook setup and teardown around the run, or drive vx from your own scripts with run() and planRun().
What goes into a key, and what comes back
Files, env vars, tool versions and upstream tasks go into the key. Outputs and logs come back. A damaged download is a miss, never a wrong build.
See inside a run
Why did this task run? What did the run do? Where did the time go? One command answers each, in text or JSON.
One failure, and exactly what it takes down
A broken build skips only what depends on it, and each skip names the failure. One flag picks fail-fast or run-everything.
When a task misbehaves
A task hangs, gets killed, prompts for input, or calls vx run on itself. vx tells you what happened in one line.
Say exactly which tasks to run
dependsOn says what a task needs first, --filter says which projects to run. A name that matches nothing stops the run before it starts.
A remote server you can trust in production
Connect @vzn/vx-reapi to BuildBuddy or your own server. Repeat runs skip the worker, every byte is checked, and a stuck server is a miss, not a hang.
Watch a CI run while it runs
@vzn/vx-otel sends each task's span the moment it ends, so your dashboard follows the build live, for the same CPU as one send at the end.
A task sees only the env it names
Your shell holds dozens of variables, some of them secrets. A task gets a short fixed list plus the names you write down, and secrets print as ***.
Output that fits the run
One task shows all its output. Many tasks show one line each for what ran. CI shows everything. You can always pick.
The cache on your terms
Turn the cache off, refresh it, or read one layer and write another. Every clone shares one store, and a pull request never writes what main reads.
Built for the agent at the keyboard
vx gives coding agents JSON with a schema for every answer, keeps a failure's log and files, and stores run history in a SQLite file.
One command to know your workspace
vx info answers the first questions of every bug report on one screen: versions, workers, memory, cache size, flaky tasks, sandbox.
A refusal an agent can read
When vx refuses, it prints a stable code and a link to the fix. Scripts and agents check the code, not the wording.
The small things
Mistype a flag or a task and vx prints the fix. Forget the task and you get a picker. Tab completes the rest.
Your run, on GitHub
Add one plugin line and every vx run shows up on GitHub: a job summary with failures first.
A run you can read
One line per task while it runs, the full log only for what failed, and one summary block at the end. Nothing prints below it.
The last run, on request
The terminal scrolled away? vx last --failed shows the run that broke and the command that runs only what failed.
See what a task really is
vx show prints a task the way the run sees it, after presets and plugins changed it. No guessing what your TypeScript config turned into.
Run only what a change reaches
vx run test --affected runs what your branch changed and everything downstream of it, and says why each task was picked.
See the plan before you run it
vx run --dry shows which tasks would come from the cache and which would run, with keys and expected times. Nothing runs.
What vx is, and what it refuses to be
vx runs and caches your task graph, correctly, and nothing else. How a run works, and what will never be inside.
Why vx is fast: five decisions, not a trick
What vx adds to a run with nothing changed, with no daemon, and the five design choices behind that number.
There was no choice on the market
Turborepo runs and caches. Nx adds a platform. vx is the runner with open plugin points, and nothing behind a paid cloud.
Performance, modularity, extensibility. In that order.
Three priorities in a fixed order settle every design choice in vx: performance, then modularity, then extensibility. Nine rules follow.
Your cache key is already in git's index
vx reads file hashes from git's index instead of reading your files: 2.3 ms for 2,527 files. A commit never changes a key.
Hash what the config turns into
vx hashes your config after it runs. Change a preset you import and every task that uses it gets a new key, with no list to maintain.
After a cache hit, dist holds only what was cached
vx clears a task’s declared outputs before it runs and after a hit, so a deleted file never lingers in dist and gets shipped.
Every cache key is known before anything runs
A task's key includes its dependencies' input keys, never their outputs. So every key in the graph exists before the first task starts.
No daemon, and still quick when nothing changed
vx has no background process watching your files. A run with nothing to do stays quick, and it can never act on a stale view of the tree.
Task priority in milliseconds, not 8.5 seconds
On a 3,270-task graph, task priority took 8.5 seconds with set unions. Bitsets do it in single-digit milliseconds, and cache restores never take a slot from a build.
Explicit over magical: why vx never guesses your inputs
vx never guesses what a task reads. A trace says what a task read once, after it ran. A cache key has to be right before the task runs.
The sandbox catches the input you forgot to declare
A cache is only correct if the declared inputs are every file the task reads. Instead of guessing that set, vx runs a task with the declared paths as the only workspace files it can touch, and fails the run on an undeclared one of its own.
vx why names the change that made a task run again
You changed one file and got twelve rebuilds. `vx why` names the part of the cache key that changed, and follows it to the root cause.
A lockfile bump rebuilds only what it reaches
Core keys no lockfile, and folding the whole file into every key would make one bump rebuild everything. @vzn/vx-lockfile keys each task on its own project's dependencies, so an astro bump here rebuilds 8 of 151 tasks.
Config in TypeScript, and why there are no named inputs
A vx.config.ts is TypeScript, so you share settings with an import. That removes globalDependencies and namedInputs from the schema.
vx lock: run configs once, check them in CI
vx lock runs every config once and saves the result. Every run reads it, and vx lock --check catches drift a file hash can't see.
One command per task; the shell is the API
A vx task is one shell command. That one rule is why remote runs, the sandbox and cache replay all work without special cases.
One binary, nothing to install underneath
vx ships as one self-contained file per platform. No Node to pick, no Bun to install, no daemon.
A plugin fills one stage, often in under 25 lines
vx is a pipeline with a named hook at every stage. A plugin fills only the stage it needs, and most example plugins are under 25 lines.
Running here is not a plugin
Your machine is always the last place a task can run, and the local cache is always asked first. A dead remote never breaks a build.
Observability that cannot break a run
A broken telemetry plugin costs a warning, never a red build. A sink that throws is turned off for the run, and one that hangs is cut off after 3 seconds.
Run tasks on a remote worker pool, keep the scheduler local
@vzn/vx-reapi runs tasks on any Bazel Remote Execution API worker pool. The graph, the placement decision, retries, timeouts and the cache all stay on your machine; what moves is one self-contained action per task.
Give your coding agent the build's memory
Your coding agent asks what you ask: what can I run, why did that re-run, which tasks flake. vx answers each as JSON from the CLI.
Dev servers as graph nodes: readiness instead of sleep
vx waits for the line your dev server prints before starting the tests, and stops the server when the run ends. No sleep 5.
Ctrl-C leaves nothing running
Press Ctrl-C and every process vx started is gone within two seconds. Every way a run can end goes through the same teardown.
Watch: a content gate, not an event storm
vx watch lets the cache key decide what runs. A README edit runs nothing, a source edit runs exactly what it reaches.
Flaky is a claim only declared inputs can back
vx calls a task flaky only when the same inputs both passed and failed. Your local run history is enough, no service needed.
From Turborepo to vx in one command
One command turns a Turborepo workspace into native vx config. Same dependsOn syntax, same --filter, and nothing dropped silently.
From Nx to vx: keep the graph, drop the daemon
One command writes native vx config from the project graph your installed Nx resolves, plugin-inferred targets included.
Benchmarks you can re-run
One synthetic task graph, each tool in its own native config, every number one command away.
Hello, vx
vx runs your monorepo's tasks and caches them, on Bun, with nothing behind a paid cloud. Here it is in one picture and one run.