inputs
inputs lists what a task reads: files, environment variables and
command outputs. The cache key is built from them.
build: { exec: { command: 'tsc -b' }, inputs: { files: ['src/**', 'tsconfig.json'], env: ['NODE_ENV'] },}inputs?: InputsConfigWhat the task reads, and so what its key folds. A task is keyed on its
inputs whether it caches or not, as Turbo and Nx hash every task: a
dependant folds that key, so an edit to an uncached generator’s inputs
re-runs the cached task behind it. A group with inputs is a keyed
group (Group tasks, below). The default build is
{ dependsOn: ['^build'], inputs: { files: ['**'] } }.
A task that runs a command and declares no inputs has no key: nothing
says what it read. A key that folds it is this run’s alone, so every
task behind it misses and saves nothing, never a stale hit. Declare
inputs on an uncached task a cached one depends on;
inputs: { files: [] } is a declaration too: it keys the task on its
config and its dependencies’ keys, and a task behind it can hit. An
absent inputs and an empty one differ.
InputsConfig
Section titled “InputsConfig”interface InputsConfig { files: readonly string[] // required workspaceFiles?: readonly string[] // optional; workspace-root-relative env?: readonly string[] // optional runtime?: readonly string[] // optional; project-dir shell commands workspaceRuntime?: readonly string[] // optional; workspace-root shell commands tasks?: readonly string[] // optional; same micro-syntax as dependsOn}inputs.files (required)
Section titled “inputs.files (required)”Project-relative globs. !-prefix negates. A literal entry (no
glob character) names a file or a whole directory tree — src and
src/ both mean everything under src, as in Turbo and .gitignore;
!src subtracts the tree. A ! entry subtracts wherever it sits in the
list, so a literal file it covers (['src/**', '!src/gen/**', 'src/gen/keep.ts']) would never be an input: that is refused at load.
Spellings a matcher would otherwise turn into
nothing are normalized: a leading ./ (./src/** is src/**, !./gen
is !gen), an inner /./ segment, a doubled //, and a trailing / on
a pattern (src/*/ is the trees under src, src/*/**). A bare . or
./ names the project directory itself and is refused at load — write
**. Turbo’s $TURBO_DEFAULT$ and $TURBO_ROOT$ and Nx’s
{projectRoot}, {workspaceRoot}, {projectName} and {options.…}
(D-52), pasted from turbo.json or
project.json, are refused in every glob list with what to write instead
(D-50): vx expands none of them, so each matched no file and a task
keyed on $TURBO_DEFAULT$ alone replayed a stale output. A glob that
starts with ^ (Nx’s ^production, the dependencies’ named input) is
refused too (D-51): vx keys a task on its dependencies through
dependsOn. A bare named input (default) can be a directory, so it
is taken as one and only warns when it matches nothing.
The wildcards are *, **, ?, a character class [ab] ([a-z],
[!a]) and a brace set {a,b}, which matches what its alternatives
match on their own: {src/**,lib/**} is src/** plus lib/**. A
backslash escapes: a route directory (Next.js, SvelteKit, Astro) is
app/\[id\]/**; bare, app/[id]/** is the class that matches app/i
and app/d, never the route. An input brace of one alternative ({b}.ts) is refused at vx lock: it would match b.ts and never a file named {b}.ts; write the
one you mean, \{b\}.ts for the braces. So is an input under .git
(.git/config): git never lists it, so it would key nothing. An extglob (@(a|b), !(x),
+(…), *(…), ?(…)) is refused too: vx’s own matchers read it as
literal text or a plain wildcard, so the key would miss the files it
names; write {a,b}. A brace range is Bun.Glob’s (Bun 1.4.4):
{1..3}, {3..1}, {01..03}, {a..c}, {1..10..3}; vx expands it as
a comma brace. A mixed-case range ({A..c}) or one of more than 1024
names is refused wherever it sits in the glob. A leading ! is still the negation, so !(group)/**
takes a route group back. This holds for every task glob — inputs.files,
inputs.workspaceFiles, outputs.files, outputs.workspaceFiles — and
for everything read from them (--affected, vx watch, the
overlapping-output refusal). Package-manager member globs (workspaces,
pnpm-workspace.yaml) and --filter path globs keep the package
manager’s grammar — all but extglob (packages/!(x)),
which a member glob refuses by name: Bun.Glob cannot read it, and
list the exclusion as its own !packages/x entry instead.
files: ['**/*'] // all project filesfiles: ['src/**', '!**/*.test.ts'] // narrow with exclusionfiles: [] // no file inputs at allfiles: ['src/**', 'tsconfig.json', 'package.json'] // specific pathsfiles: ['src', 'package.json'] // a directory literal is its treefiles: ['app/\\[id\\]/**'] // the route dir `[id]`; bare `[id]` is a classEmpty array is valid — the cache key still incorporates command, env,
upstream hashes and any plugin’s key material. A task with no file
inputs (e.g. a pure “download-and-verify”) still cache-misses on a
lockfile change when a lockfile plugin (@vzn/vx-lockfile) is declared.
Always applied to every glob pass (regardless of what you wrote):
- gitignore filter — workspace-root + project
.gitignore, unless an entry names the ignored path outright: a literal (.env.local), or a glob whose fixed prefix git ignores (dist/**,dist/*.js,.cache/gen/**), which is then walked on disk under that prefix only. A directory git ignores but for a file it lists (gen/*with!gen/.gitkeep, orgen/with a force-added file) counts as ignored:gen/**reads the untracked files beside the kept one. Each alternative of a brace counts on its own:{gen/a.txt,gen/b.txt}names both files and{gen,out}/**both trees, asgen/{a,b}.txtdoes. Such a file is hashed from disk and never stored or restored; a missing one is not an input until it exists. A broad glob (**,src/**over a listedsrc) still skips ignored files, so it never walksnode_modules. Its bytes differ per machine, so machines with different files miss each other’s shared entries. An ignored file never selects a task under--affectedon its own, as in Nx (only what git sees does);vx watchre-runs it on an edit there. - Always-ignored —
.git/**,.vx/cache/**,.vx/projects.lock.json,vx.lock.json, and.vx/tmp/**, where vx’s restores stage, only where vx writes them: at the project’s root (a project’s ownsrc/.vx/tmp/xorsrc/sub/vx.lock.jsonis an input), and forworkspaceFilesevery project’svx.lock.json. Nothing else: a tool’s temp files are the task’s to exclude. A broad glob on abun build --compiletask, or on one that runs beside it, excludes Bun’s intermediate and its cross-compile extraction directory itself:['**/*', '!**/*.bun-build', '!**/.????????????????-????????.tmp/**']. - Installs — an untracked file under any
node_modules/that git does not ignore. An ignored one follows the gitignore filter above like any other ignored path: named outright, it is an input (node_modules/.prisma/client/**, a generated client); a broad glob skips it because it is ignored. A file git TRACKS there, such as a committed test fixture, is an input like any other. - Declared
outputs.filesare excluded — a task never invalidates itself via its own output. A path an output!entry takes back is no output, so it stays an input (A-44). Another task’s outputs are not excluded for you: an input glob that can match them is refused (X-54), unless git ignores them whole, or the producer is a dependency the key does not fold (see Inputs over outputs in § Settings). Write the exclusion,['**/*', '!dist/**'], or ignoredist/. An input entry the task’s OWN outputs take back whole is always refused (X-55): vx removes outputs before a run, so a formatter declaringsrc/**as both would delete its sources. A task that rewrites files in place declares no outputs. - Nested-project subtree — files belonging to a project rooted
inside this one’s dir are excluded. No cross-project leakage via
globs; the only cross-project relationship is
dependsOn+ upstream-hash propagation.
inputs.workspaceFiles (optional, default [])
Section titled “inputs.workspaceFiles (optional, default [])”Workspace-root-relative globs — the Turbo $TURBO_ROOT$ / Nx
{workspaceRoot} equivalent, for inputs that live outside the project
dir (a root tsconfig.base.json, shared codegen output, …). Same
syntax as files (! negation subtracts, in any order), same git-aware
resolution (tracked + untracked-not-ignored; a gitignored file only when
named outright, as in files), and the resolved paths join the same
cache-key file list.
inputs: { files: ['src/**'], workspaceFiles: ['tsconfig.base.json', 'shared/**', '!shared/README.md'],}No boundary rule — deliberate escape hatch. Unlike files,
workspaceFiles globs may match files anywhere in the workspace,
including inside other projects’ directories. That’s bad practice
(prefer project-relative declarations plus dependsOn + upstream-hash
propagation for cross-project relationships), but it is there for the
cases that genuinely need root-anchored inputs. The hard
project-boundary rule continues to apply to project-relative files
globs only.
No input or output glob, files or workspaceFiles, may be absolute
or hold a .. segment, and a brace arm counts: {../shared,src}/** and
{/etc,src}/* are refused at load, where the glob engine would have
matched nothing under that arm and said so nowhere. A Windows spelling
is refused the same way, with the forward-slash glob to write: a
backslash separator (src\**, src\*.ts) or a drive (C:\src\**,
C:/src/**). A backslash before a bracket, a brace, a ! or a
backslash stays an escape.
Still applied: the always-ignored set (.git/**, .vx/cache/**,
.vx/projects.lock.json, vx.lock.json, .vx/tmp/**),
untracked files under node_modules/, and the task’s own declared
outputs, outputs.workspaceFiles and the outputs.files its globs reach
in its own project (a task never invalidates itself).
vx watch: when any config declares inputs.workspaceFiles, the loop
watches the workspace root recursively (any file can be an input once
boundaries are off), so root-subdir changes re-trigger cycles like any
other edit. The always-ignored set still filters the noise.
inputs.env (optional, default [])
Section titled “inputs.env (optional, default [])”Env var names whose host values are folded into the cache key.
Independent of exec.env:
- Listing a name in
exec.env.passThroughdoes NOT make its value affect the cache. - Listing a name here does NOT forward it to the child process.
To both forward AND track, list it in both places. Yes, the
double-declaration is mild noise; a preset helper
(envTracked('NODE_ENV')) can sugar it.
Names are exact: a wildcard (NEXT_PUBLIC_*) is refused, and so is
Turbo’s !NAME exclusion, here, in exec.env.passThrough and in
exec.env.secret (D-53). With no wildcard to take it out of, !NAME
was a variable of that name, and nothing was excluded.
inputs: { files: ['src/**'], env: ['NODE_ENV', 'TARGET_BROWSER'], // changes here bust the cache}A cache-input env var the task actually reads must ALSO appear in
exec.env.passThrough (or define) — the child environment is
isolated, so without it the key varies on a value the command never
sees. Tracking-only declarations (an env var that influences inputs
some other way) are legal but rare; if in doubt, declare both.
An unset name, a name set to the empty string, and a name never
listed are three different keys: the key holds the count of names,
the names, and each value, with “unset” folded apart from every value
(the child tells unset from empty too — process.env.X ?? 'dflt',
${X-dflt}). A name holding a NUL is refused.
inputs.runtime (optional, default [])
Section titled “inputs.runtime (optional, default [])”Shell commands whose trimmed stdout and stderr, kept apart, are folded
into the cache key — the runtime-output analog of inputs.env. It runs
in the project dir; the Nx
runtime input,
which Nx runs at the workspace root, is workspaceRuntime below (Turbo
has no built-in for this — see
vercel/turborepo#4124).
Use it for tool/runtime versions, OS info, or a project-local probe
script whose value should bust the cache when it changes.
inputs: { files: ['src/**'], runtime: ['./scripts/probe.sh', 'rustc --version'],}Semantics:
- Each command runs in the project dir (cwd = the project’s
directory), via
sh -c, so pipelines and redirects work (“shell is the API”). Deduped per(projectDir, command)within a run — a command declared by bothbuildandtestin the same project runs once. - The output is resolved live at hash time on every run (inside
resolveInputs), exactly likeinputs.envreads host env values live.vx lockfreezes the command strings (they’re in the resolved config), not their output, so the field stays correct under the lock every run reads — the changed output of a locked command still busts the cache. - A non-zero exit fails the run (a hard
UserErrornaming the command and exit code) — fail-loud, like a missing git binary. A flaky probe should not silently degrade to a stale hit. - Output that is not UTF-8 fails the run too: a lossy decode keyed
every invalid byte alike. Pipe binary output through a hash or
od. - The command inherits vx’s full environment, not the isolated
env that task
execcommands get —exec.env.defineandpassThroughdescribe the command’s environment, not the probe’s. This is deliberate —node -v/rustc --versionneed the realPATHand toolchain env, and it is what lets two tasks sharing a probe share one spawn (the per-run dedup above). The flip side (same asinputs.env): an env var that differs between machines silently changes the key; declare such inputs explicitly if you want them visible. - Its
PATHdoes lead with the samenode_modules/.bindirectories the task’s does (the project’s, then the workspace root’s), sotsc --versionkeys thetscthe task runs. Before item 996 it keyed the global one, or failed with 127 where only the workspace had it, and a changed local tool replayed the old output. - The command runs whenever a task’s key is derived — that’s every
run (warm runs included, since the key decides hit vs miss), plus
vx run --dry/--graph(which predict the key) andvx run --no-cache(which still derives the key). Keep runtime commands pure probes with no side effects. - The command runs as part of hash derivation, before the task’s
exec— so it is not constrained by the task’s ownsandboxpolicy (the probe runs unsandboxed even on a sandboxed task).
inputs.workspaceRuntime (optional, default [])
Section titled “inputs.workspaceRuntime (optional, default [])”Like runtime, but commands run at the workspace root (cwd = the
workspace root) and are deduped globally per command across the
whole run — a node -v declared in 500 projects spawns exactly once.
The runtime-input analog of workspaceFiles: per-task, root-anchored.
Use it for global tool versions, OS info, or any probe whose value is
the same for every project. Its PATH leads with the workspace root’s
node_modules/.bin only (no project’s: the value is shared by all).
inputs: { files: ['src/**'], workspaceRuntime: ['node -v'], // runs once per run, root cwd}Same sh -c execution, live-at-hash-time resolution (frozen-safe), and
non-zero-exit-fails semantics as runtime. The two fields are folded
into the cache key in distinct namespaces, so an identical
(command, output) pair never aliases between them.
inputs.tasks (optional, default = all upstream)
Section titled “inputs.tasks (optional, default = all upstream)”Same micro-syntax as dependsOn, plus two filter-only extras:
| Form | Meaning |
|---|---|
'*' |
Include every same-project upstream hash. |
'^*' |
Include every dep-workspace upstream hash. |
'name' |
Include same-project task name. |
'^name' |
Include name in every dep workspace. |
'pkg#name' |
Include the specific package’s name task. |
'!<form>' |
Exclude — any of the above with a leading !. |
Patterns work here too, in EITHER half of every form:
'build.*', '^build.*', 'pkg#build.*', '@acme/*#build',
'!codegen.*' — the same *-glob dependsOn patterns use. A filter
only ever selects from upstreams that already exist, so a package
pattern is unambiguous here, unlike dependsOn, which must
materialize concrete edges and rejects it. (A filter that matched
patterns literally would silently select zero hashes and decouple the
task from its dependencies — a stale-hit trap.)
Patterns are applied in order; last write wins. So
['*', '^*', '!^noisy'] reads as “all upstream except deps’ noisy
task”. Defaults:
- Omitted → all upstream contribute (
['*', '^*']). Most common. []→ fully decoupled; no upstream contributes.
An exact entry ('codegen', '^build', 'lib#build') must be
named by some dependsOn entry of the same task — exactly, or by that
entry’s own * pattern (dependsOn: ['build.*'] names
tasks: ['build.bun']), and in a form that reaches the same task:
build is this project’s, ^build only the dependencies’, so
tasks: ['build'] against dependsOn: ['^build'] names nothing. A
pkg#task entry may be this project or a dependency, so it pairs with
either form; two pkg#task entries must agree on the project. One that
is not named (['buidl']) is refused at load: it would match nothing at hash time and fold no upstream hash,
decoupling the task from its dependencies with no diagnostic — a stale
hit waiting for the next upstream change. Patterns, wildcards and
negations stay silent (a preset-spread pattern legitimately matches
nothing in some projects); [] is the explicit way to decouple.
A filter selects among the task’s own dependencies, never through a
group: over dependsOn: ['build'], where build is a group of
build.a and build.b, the filter sees build, not its members. A
pattern that matches no dependency but does match a group’s member
('build.*' there) is refused when the run plans: it would select
nothing and stop the key moving with that member. Name the group, or
depend on the member directly.
Examples:
tasks: ['^build'] // only ^build from deps; nothing from selftasks: ['codegen', '^*'] // self.codegen + everything from depstasks: ['*', '^*', '!^noisy'] // all upstream except deps.noisytasks: ['lib#build'] // a single cross-project hashtasks: [] // fully decoupledWhen to use this: when a task dependsOns another for ordering
(it has to run after) but the upstream’s outputs do NOT affect this
task’s outputs. Typical case: an integration-test task dependsOns
build for ordering, but its inputs are only the test source — so
inputs.tasks: [] to keep build from invalidating it.