Skip to content
vxvx
GitHubBlueskydev.toRSS

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?: InputsConfig

What 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.

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
}

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 files
files: ['src/**', '!**/*.test.ts'] // narrow with exclusion
files: [] // no file inputs at all
files: ['src/**', 'tsconfig.json', 'package.json'] // specific paths
files: ['src', 'package.json'] // a directory literal is its tree
files: ['app/\\[id\\]/**'] // the route dir `[id]`; bare `[id]` is a class

Empty 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, or gen/ 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, as gen/{a,b}.txt does. 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 listed src) still skips ignored files, so it never walks node_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 --affected on its own, as in Nx (only what git sees does); vx watch re-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 own src/.vx/tmp/x or src/sub/vx.lock.json is an input), and for workspaceFiles every project’s vx.lock.json. Nothing else: a tool’s temp files are the task’s to exclude. A broad glob on a bun build --compile task, 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.files are 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 ignore dist/. 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 declaring src/** 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.

Env var names whose host values are folded into the cache key. Independent of exec.env:

  • Listing a name in exec.env.passThrough does 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.

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 both build and test in the same project runs once.
  • The output is resolved live at hash time on every run (inside resolveInputs), exactly like inputs.env reads host env values live. vx lock freezes 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 UserError naming 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 exec commands get — exec.env.define and passThrough describe the command’s environment, not the probe’s. This is deliberate — node -v / rustc --version need the real PATH and toolchain env, and it is what lets two tasks sharing a probe share one spawn (the per-run dedup above). The flip side (same as inputs.env): an env var that differs between machines silently changes the key; declare such inputs explicitly if you want them visible.
  • Its PATH does lead with the same node_modules/.bin directories the task’s does (the project’s, then the workspace root’s), so tsc --version keys the tsc the 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) and vx 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 own sandbox policy (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 self
tasks: ['codegen', '^*'] // self.codegen + everything from deps
tasks: ['*', '^*', '!^noisy'] // all upstream except deps.noisy
tasks: ['lib#build'] // a single cross-project hash
tasks: [] // fully decoupled

When 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.

Turborepo, Nx and other product names are trademarks of their owners. vx is not affiliated with or endorsed by them.