A pipeline that applies no plugin by default has to answer one
question honestly: what happens with no plugins at all? For vx the
answer is that the workspace runs and caches exactly as it would with
a full workspace file, because the local executor and the local cache
are not plugins. They are the floor.
Two lists, one tail
When vx places a task, it walks the executor list from
vx.workspace.ts in declaration order and asks each one whether it
will take the task. When vx looks a key up, it walks the cache layers
in order. Both lists have the same implicit last element:
executors: [ reapi, …, local ]
cache: [ turboCache, …, local cache ]
You never write the last element and you cannot remove it. A workspace
with no vx.workspace.ts is the empty list plus the floor, which is
why vx run build works in a fresh repository with one vx.config.ts
and nothing else.
Update (2026-10-07): the floor's artifacts now live in a shared store,
~/.vx/<id>/cache, that every checkout of the repository hits; each
workspace keeps its own index in .vx/cache. A named cacheDir holds
both. See the shared store.
Declining is a first-class outcome
Because the floor is always there, a plugin is allowed to say no. That
turns a lot of would-be failure modes into placement decisions:
- A remote executor declines a task that has no
cacheblock (a worker would run it against an empty tree). It runs here. - A persistent task and everything that depends on it is never offered to a remote executor, because a worker cannot reach a port on your laptop. They run here.
- A sandboxed task is never offered either: the sandbox is local machinery a worker does not have, and a boundary verified remotely would pass vacuously. It runs here, inside the sandbox, and so does everything that depends on it.
- A remote cache that errors, times out or is simply unreachable degrades to a miss on that layer, and the lookup continues down the chain to the local cache. A remote outage is a slower run, not a broken one.
-
@vzn/vx-reapiagainst a server that only advertises caching declines the executor with a warning; the cache layer still works.
vx run --dry shows the decision per line once the workspace declares
more than one executor: @vx/reapi, @local, or @noop for an
exec.remote: 'only' task no remote takes, which the run skips.
Why it is the floor and not a default plugin
It would have been easy to ship localExecutorPlugin() and
localCachePlugin() and prepend them when the list is empty. That
design has a hole: a list that is not empty would have no floor
unless the user remembered to append them, and a plugin that declined
would have nowhere to hand the task. Making local behaviour the tail of
every list instead of a member means the "no plugin" case and the
"plugin declined" case are the same case, and there is no configuration
in which a task has nowhere to run.
It also keeps the principle intact. Core names no plugin. What it
names is a machine and a directory, which is not a capability anyone
supplies. Everything above the floor, including the wire a remote cache
speaks, is declared in the workspace file or it does not exist.
Originally published on the vx blog. vx is an MIT task runner and build cache for JS monorepos: vznjs.github.io/vx · GitHub.
Written with AI assistance.
Top comments (0)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.