
Start from Turbo or Nx in one line
vx init now sets up a Turbo or Nx repo with one plugin line, the workspace root can hold tasks, and vx last jumps straight to the run that failed.
nx() maps an Nx project graph to vx tasks with nothing written, --affected selects dependents too, and remote caches stream artifacts instead of holding them in memory.

vx 0.0.22 through 0.0.75 shipped between 2026-09-15 and 2026-09-27. The theme is adoption and correctness: nx() maps an Nx graph as it is, CI selection does what its name says, and several stale cache hits are gone. A few changes are breaking; they are listed at the end.
In this release
nx() from @vzn/vx-migrate turns every target in Nx’s resolved project
graph into a vx task. You write no config. Executor targets run through
nx-exec, one process per executor on Nx’s own runExecutor, and
run-commands targets run as the shell commands they are. Configurations
become <target>:<configuration> tasks. nx-exec starts in 214 ms
against 346 ms for nx run with the daemon warm.
import { defineWorkspace } from '@vzn/vx/config'import { nx } from '@vzn/vx-migrate'
export default defineWorkspace({ plugins: [nx()] })PRs #685, #688, #691. Docs: Migrate from Nx. Deep dive: From Nx.
--affected used to select only the changed projects. On a branch that
edited lib, a CI gate ran lib’s tests and skipped app, which depends
on it. It now selects the changed projects and everything that depends on
them. --filter '[<base>]' keeps the changed-only form, and the new
--filter '...^<pattern>' selects a package’s dependents without the
package itself.
vx run test --affected=origin/mainflowchart LR U["ui changed"] --> UT["ui#test"] U --> W["web#test"] U --> D["docs#test"] style U stroke:#c6f84e,stroke-width:2px
PR #446, commit c148bd6. Docs: Run and filter.
cacheRetention in vx.workspace.ts evicts local cache entries at the end
of a run, by the same policy vx cache prune takes as flags. olderThan
drops entries unused for that long, then maxSize drops the least recently
used past the size. It does work only when something is due.
import { defineWorkspace } from '@vzn/vx/config'
export default defineWorkspace({ cacheRetention: { olderThan: '30d', maxSize: '10G' },})PRs #755, #772. Docs: Workspace config.
A remote cache layer now streams an artifact between disk and the wire instead of holding it in memory. On a 150 MiB round trip, peak memory grew by 45 MiB instead of 495 MiB. Every first-party layer moved with it. If you wrote your own layer, see Breaking changes below.
// a remote cache layer's get and put, as they read todayget(hash: string): Promise<{ body: Blob | Response; durationMs: number | undefined } | null>put(hash: string, body: Blob, meta: { durationMs: number }): Promise<void>The docs site has a “Try it” page that runs vx’s real planner in your
browser, bundled from core’s own source. Edit a file, an environment
variable or a vx.config.mjs, press Run, and read each task’s key, hit or
miss, and why a key moved. Core’s tests hold the page’s plan to what
vx run --dry=json prints.
vx run build --all --dry=jsonPRs #789, #791, #793, #797. Page: Try it.
A bracket in cache.inputs.files, cache.outputs.files or
workspaceFiles now means a bracket. Read as a character class, an input
over a Next.js route like app/[id]/ keyed nothing, so an edit replayed
an old output. An output over one deleted app/i/page.js and saved
nothing. \[id\] still works. CACHE_VERSION moved to v28.
cache: { inputs: { files: ['app/[id]/**'] } }PR #765.
A project config must be JSON data on every path, because the cache key
folds its JSON. A function, symbol, bigint, NaN, Map, Date, cycle or
undefined inside an array is refused before the schema, with the path
that holds it. Before, vx watch accepted a function that vx run
refused, and a Map or Date loaded on both and was keyed as {} or a
string. An undefined property is still fine, and vx.workspace.ts is not
held to this rule.
vx run build --allPR #792.
Two bugs could replay wrong output under a green run. Bun’s xxHash3 reads
only 32 bits of a seed, so the key fold carried 32 bits of state and two
input sets could share a key. The fold now feeds the seed forward
(CACHE_VERSION v29). And npm and Yarn link every workspace package at the
root, so a sandboxed task could read its own project through
node_modules without the read being reported. That read now fails the
task (CACHE_VERSION v30). Each bump makes every entry miss once.
vx run build --allPRs #775, #806, #812. Deep dive: The sandbox.
A watch cycle that re-reads configs now starts one config worker for the round, not one per config. With 50 configs a round went from 148 ms to 10.5 ms.
vx watch test --allPR #785.
!/x) in cache.inputs.files fails to load
(#774).get resolves { body: Blob | Response } and put
takes a Blob; a layer that returns bytes is refused
(#761).See Upgrading.
npm install -D @vzn/vx@latestA standalone binary updates itself with vx upgrade.