Dev & long-running tasks
Some tasks don’t finish — a dev server, a file watcher, a background daemon. vx models these as persistent tasks: it spawns them, decides when they’re “ready,” lets downstream tasks proceed, and tears them down when the run ends.
Declaring a persistent task
Section titled “Declaring a persistent task”dev: { exec: { command: 'vite', persistent: { readyWhen: 'Local:', readyTimeoutMs: 30_000 }, },}vx run devpersistent tells vx not to wait for the process to exit. Instead:
- With
readyWhen, the task is “ready” the moment a line of its output matches that regex (here, Vite’sLocal:banner). The match also sees trailing partial lines, so a no-newline prompt likeListening on :3000works. readyTimeoutMsbounds the wait. If readiness never matches in time, vx kills the process and fails the task instead of hanging forever. (It requiresreadyWhen.)- With no
readyWhen(persistent: {}), the task is ready as soon as it spawns — fine for daemons with no observable ready signal that nothing needs to gate on.
Gating downstream work on readiness
Section titled “Gating downstream work on readiness”The point of readyWhen is to start dependent work only once the server
is actually up. Classic case: end-to-end tests against a dev server.
tasks: { dev: { exec: { command: 'vite', persistent: { readyWhen: 'Local:', readyTimeoutMs: 30_000 } }, }, e2e: { dependsOn: ['dev'], exec: { command: 'playwright test' }, cache: { inputs: { files: ['e2e/**'], tasks: [] }, outputs: { files: ['playwright-report/**'] } }, },}vx run e2e # starts dev → waits for "Local:" → runs playwright → tears dev downtasks: [] keeps the dev server’s hash out of e2e’s cache key — the
dependency is for ordering, not output identity. See
Task dependencies.
Lifecycle and cleanup
Section titled “Lifecycle and cleanup”- Exit before ready ⇒ failure. If a persistent task crashes or exits
before
readyWhenmatches, vx reports it as failed (and frees anything waiting on it). - Automatic teardown. Once the rest of the graph finishes — success
or failure — vx sends
SIGTERMto every persistent process and waits for them to exit before returning. No orphaned dev servers left running in CI, andCtrl-Creaps them too. - No caching. Persistent tasks can’t have a
cacheblock — there’s no exit code to cache and no well-defined moment to capture outputs. The config loader rejectspersistent+cache.
Watchers: persistent vs. vx watch
Section titled “Watchers: persistent vs. vx watch”These are two different things:
- A persistent task with a watcher command (
tsc --watch) runs that watcher as a long-lived process for the duration of the run.typecheck: {exec: { command: 'tsc --watch --preserveWatchOutput',persistent: { readyWhen: 'Watching for file changes' } },} vx watch <task>is vx itself re-running a normal, cached task whenever files change — you get cache hits between runs. Reach for this for a fast test/lint/typecheck loop:Terminal window vx watch test
Use vx watch for “re-run this cached task on change”; use persistent
for “keep this process alive and gate other tasks on it.”
Next steps
Section titled “Next steps”- Task dependencies — gating order and cache identity.
- Running & filtering tasks —
vx watchand run flags. - Configuration reference — every
persistentfield.