vx runs and caches your task graph, correctly, and stops there.
Here is what that means in practice.
A run is a pipeline
-
Lock.
vx lockruns eachvx.config.tsonce and saves the result. A run reads only that. - Graph. One task graph for the whole repo.
- Key. Each task gets a hash of its inputs, its config and the keys of everything before it.
- Schedule. Restore hits. Run misses. Save.
In plugin terms the stages are named, in order:
-
config→discover→project→graph→key→fingerprint→schedule→admit - then
executor,cache,telemetry,output,setup,teardownandcommands
A plugin can fill any of them.
Core turns on no plugin by default. With none, vx still runs and
caches on your machine.
A task is one command
build: {
exec: { command: 'tsc -b' },
inputs: { files: ['src/**', 'tsconfig.json'] },
cache: { outputs: { files: ['dist/**'] } },
}
-
Caching is opt-in. No
cacheblock, no cache. vx never guesses what a task reads. - One command per task. A plugin may change where it runs, never what it is.
- A project reads only its own folder. What it needs from another project comes through the graph.
What is not inside
| Never in vx | Why |
|---|---|
| A cloud, account or dashboard | vx does not need to know your company exists. |
| A daemon | A second copy of the truth can go stale. Git is read fresh per run. |
| Inferred or traced inputs | A trace says what a task read once. A key is needed before it runs. |
| JavaScript-function tasks | The shell is the API. Your tools stay yours. |
| Named, global inputs or env | Configs are TypeScript. A shared preset is an import. |
Remote caches, remote execution, telemetry and CI summaries are plugins
in their own packages. @vzn/vx-reapi (Bazel's Remote Execution API)
shows the plugin points are wide enough.
With no cloud to sell, vx has one job: be correct and fast on the
machine in front of you.
Next: Why vx is fast. Reference:
Architecture. More at
vznjs.github.io/vx.
Originally published on the vx blog. vx is open-source (MIT) task orchestration with a build cache: vznjs.github.io/vx · GitHub.
Written with AI assistance.
Top comments (2)
The explicit-input approach makes sense, but it puts a lot of weight on getting the dependency contract right. Take an OpenAPI codegen task in a monorepo: if its cache key includes the schema but misses the generator version or configuration, vx can restore a perfectly valid cache hit that produces an outdated client.
That's the trade-off with declared inputs: you get predictable caching, but only if the declaration captures everything that can affect the output. I'd be interested in how vx's sandbox helps developers catch those undeclared reads before they turn into subtle CI-only failures.
Good example, and that's the risk we built around. The generator version is covered already: a task's key includes its package's package.json, and with the lockfile plugin, the exact resolved versions of its dependencies. So bumping the generator is a miss, not a stale hit.
(Lockfiles: vznjs.github.io/vx/guides/configur...)
For config and anything else the task reads, turn on
exec.sandboxfor the task. A read of a file you didn't declare is then denied and the task fails, and a failed task is never cached. Run it sandboxed locally or in CI once, and an undeclared generator config shows up as a red task with the path in the error, instead of a quiet stale client later.(Sandboxing: vznjs.github.io/vx/guides/sandboxing/)
Once a violation is found, you decide: allow the read in the sandbox's
allow.read, or leave it out and it stays blocked. Allowing a read doesn't make it a cache input; if the file should change the key, list it incache.inputs.filestoo. Keeping the two explicit is the point.(Caching: vznjs.github.io/vx/guides/configur...)