A migration or seed can sit in front of the tests that need it, a server stays down when the remote holds every test, vx init gives ^build its edges, and vx answers only its own flags and verbs.
A setup step such as a migration or a seed can now be a dependency. A
dev server stays down when the remote cache already holds every test
that needs it. vx init gives ^build its package edges in one step,
and vx why names the task that keeps a key from ever hitting. vx now
answers only its own flags and verbs.
This release is breaking: other runners’ flag aliases and command hints
are gone. Read the breaking changes first.
A persistent task that exited 0 before its readyWhen matched used to
fail, so a migration, a seed or docker compose up --wait could not sit
in front of the tests that need it.
Now a persistent task that exits 0 before it is ready counts as ready,
and its dependants run. One that exits non-zero still fails.
vx run e2e already left the dev server down when every test it serves
was a local hit. On a fresh CI runner the hits come from the remote, so
the server started anyway.
Now vx asks the remote about those tests first. When it holds them all,
the server never starts; at the first miss it starts, and that test runs
once the server is ready.
flowchart LR
E[e2e tests] -- "keys asked up front" --> R[(remote cache)]
R -- "all held" --> S[server not started]
R -- "a miss" --> U[server starts, the test runs]
In a monorepo with a lockfile, vx init declared no plugin, so the
first dependsOn: ['^build'] was refused at vx lock with “needs
project edges”.
Init now declares the plugin the root lockfile calls for (bun(),
pnpm(), yarn() or npm()) and installs @vzn/vx-lockfile beside
@vzn/vx. Beside a bun.lock:
A key that folds a task with no inputs is new on every run, so the
task never hits. vx why said “inputs differ” and named neither the
cause nor the task to fix.
Now the run records that task, and the verdict names it:
Terminal window
$ vx why build
//#build — run 01a16609-2047-74f3-b570-a3194f1c3cf7
this run 2026-10-22T23:53:56.590Z · success · executed · key a2e1acbece94cd3e
vx owns its commands. vx run no longer rewrites another runner’s
flags (-t, -p, -F, --skip-nx-cache, --dry-run), and verbs such
as vx run-many, vx ls and vx reset no longer answer with a
pointer. Each is an unknown flag or command, like any other typo:
Terminal window
$ vx run build -t x
✗ vx run unknown flag: -t
see vx run --help
vx run --format json now says where a run’s JSON is: vx last --format json after the run, or --dry=json for the plan.
On GitHub Actions, GitLab, Buildkite and CircleCI the log keeps its
colour, task output included. NO_COLOR or FORCE_COLOR=0 turns it
off.
A warm run whose inputs have not moved reads every task key back from
one stored row, and a warm hit walks its outputs less.
The footer’s info row names only the cache layers the run used:
local + remote cache, local cache, remote cache or no cache.
Run rows put the machine a task ran on last, the tasks line counts
blocked tasks, a dim header names the columns, and every cache bar
segment has its count.
A stopped run keeps its finished tasks in history, and a stopped task
still names where it ran.
A Linux sandboxed task reports its CPU time and peak memory.
VX_AFFECTED_BASE names the commit a bare --affected compares with.
vx upgrade run from a bunx cache suggests bunx @vzn/vx@latest.
vx why on an uncached task says it saves no entry, so no
fingerprints name the input that moved.
A config migrated from Nx no longer carries Nx’s task env, its npm:
tags or nx-input: task names; input tasks are inputs:<name>.
@vzn/vx-history keeps each CI job’s timings under its own
task set and carries every task forward, so a later job finds its
tasks’ history.
Other runners’ flag aliases on vx run and vx watch (-t, -p,
-F, --skip-nx-cache, --dry-run and the rest) → removed.
The vx run-many, affected, graph, query, ls, reset,
daemon, login, logout, link and unlink hints → removed.
Plugin API: TaskOutcome.keyless → keylessBy, the id of the task
with no inputs the key folds. TaskExecutor.running and
RunContext.whereRunning may return null (the task waits for a
machine).
The local cache index and run history are rebuilt once (core schema
v46).
vx init installs vx for you, a cache server keeps branches apart by the CI token instead of trusting the client, a slow download gives way to running the task, and a local hit copies its files out unchecked.
A local cache hit copies plain files instead of unpacking an archive, a remote hit is decompressed once straight into the store, remote downloads and uploads set their own pace, a cache server learns each request's scope, GitLab CI gets branch scopes, [...] in a task glob is a character class, and vx upgrade re-locks.
A remote cache scope is now the branch, named owner/repo:branch, so a laptop on a branch reads what CI built there. Each task asks the remote one key, and the local cache never splits by scope. --cache-scope is gone and run.affectedBase is now baseBranch.
Turborepo, Nx and other product names are trademarks of their owners. vx is not affiliated with or endorsed by them.