Predictive scheduling
The default scheduler picks ready tasks by reverse-deps count — a task that unblocks the most downstream work runs first. That’s a reasonable static heuristic but it doesn’t know that a 30-second test blocks less wall-time than a 4-second build that unblocks 40 downstream tasks.
Predictive scheduling reads cache.db.runs for every task in your
graph, computes the expected remaining critical-path duration per
node, and dispatches by that instead. The only task runner that
learns from itself.
Quick start
Section titled “Quick start”import { defineWorkspace } from '@vzn/vx'
export default defineWorkspace({ predictive: true,})That’s it. On the next run, vx loads history, computes priorities, and uses them.
How it works
Section titled “How it works”-
Load history.
prepareRuninstantiatesLocalHistoryProviderover the cache.db handle and callsloadFor(taskIds)to get aHistoryTablekeyed byproject#task. Each entry has p50/p99 durations (cache hits excluded), success rate, hit rate. -
Compute expected critical path per node. For each node, topo-DP sums the node’s own p50 + the max across downstream chains:
ECP(n) = p50(n) + max(ECP(d) for d in dependents(n))A leaf with no dependents has
ECP = p50(itself). -
Pick highest ECP from the ready set. The scheduler’s priority map merges history-aware ECP on top of the static baseline. Override > baseline by a large factor so a node with history beats one without; among nodes without history, the baseline still tiebreaks.
-
Fall back when history is sparse. A task with no prior runs falls back to the workspace median across tasks. If the workspace itself has no history, the default is 1000 ms — a sane “I don’t know” that puts the priority in the right order of magnitude.
What this changes vs. the default
Section titled “What this changes vs. the default”Two example graphs where the heuristic differs:
Graph A: long leaf
Section titled “Graph A: long leaf” build (5s) ↙ ↘ test publish (30s) (2s)- Default: starts with
build(it unblocks 2 downstream). Then whichever oftest/publishhappens to be ordered first in the ready queue (typically graph-insertion). - Predictive: starts with
build. Once it finishes, pickstestfirst (ECP = 30s + 0 = 30s) overpublish(ECP = 2s). On a single worker, ordering doesn’t change total wall time — but it surfaces the slow path earlier (better UX) and prevents worker starvation on multi-worker graphs.
Graph B: lots of fast vs. one slow
Section titled “Graph B: lots of fast vs. one slow” db_test (90s, blocks nothing) lint (2s, blocks 40 build tasks)- Default: picks
lint(blocks 40). Critical for single-worker graphs. - Predictive: still picks
linton a single worker; on a multi-worker graph where there’s spare capacity, dispatchesdb_testearly in parallel.
The merge function (mergePriorities in src/graph/scheduler.ts)
preserves correctness: nodes covered by the override sort above
all baseline-only nodes, and within the override set the baseline
tiebreaks. Nodes the override didn’t see fall back to baseline.
When predictive helps most
Section titled “When predictive helps most”- Multi-worker graphs with mixed task durations. The historical ordering surfaces the long tail earlier so workers don’t go idle waiting for the last slow task.
- CI matrices where you care about wall-time-to-failure for human debugging. A failing test surfaces faster.
- Established repos with weeks of history. Cold workspaces get the workspace-median fallback.
When it doesn’t help
Section titled “When it doesn’t help”- Cache-warm full-hit runs. Cache hits cost ~ms; there’s nothing to prioritize.
- Single-task runs. No queue to reorder.
- Brand-new workspaces. No history; falls back to baseline + default duration.
Observability
Section titled “Observability”vx info --history surfaces the same HistoryTable the scheduler
consumes. vx mcp exposes getRunHistory so AI agents can ask
“what does the scheduler think about this task” in plain English.
Trade-offs
Section titled “Trade-offs”- Bias toward the slow path. Predictive dispatches long tasks early. If you’d rather see fast feedback (lint failures first), leave it off.
- Cold start. A new task with no history uses the workspace median. If your tasks are wildly different durations, the median is a poor estimator until you have ~10 runs of the task.
- Opt-in only today. No “default-on” plan; needs more telemetry
on the wall-time win before flipping the default. See
docs/design/predictive-execution-2026-06.mdPhase F.
What’s coming
Section titled “What’s coming”Phase C-E are deferred but designed:
- Speculative pre-warming:
posix_fadvise(WILLNEED)on input files, module preload forbun/noderuntimes. - Bandit-driven retry: flaky tasks (history success rate < 95%) auto-retry once on transient failure.
- Regression detection: at runEnd, compare this run’s durations against the rolling p50; flag significant deviations.
See also: docs/design/predictive-execution-2026-06.md.