DEV Community

Ordewell
Ordewell

Posted on Originally published at ordewell.ai

Task order: when a task is ready to run, and what can run in parallel

A plan looks like a list, but at run time it is a graph that the scheduler walks one step at a time. At every step it asks a single question: which tasks may start right now. The answer decides the order, the parallelism, and what happens when something upstream fails. Most of the confusion about "why is my plan not moving" lives in that answer.

I build Ordewell, the tool this post walks through, so what follows is how it does this rather than a survey. It is free and Apache 2.0. Every tool that runs more than one task has to answer the same question, so the reasoning is useful even if you never install it.

The scheduler answers one question, and only that

The planner emits an ordered list of tasks, each with a title, a prompt, and a list of dependencies by task number. It does not emit a running order or a wave plan. Those are derived, and they are derived the same way on every tick, so a task does not start because some step decided to start it. It starts because at that moment it passed every gate.

That distinction matters when you edit the plan: you are not steering a running process, you are changing the state the question is asked against.

The readiness rule

A task is ready to start when all of these hold:

  • It is pending or approved. A task that is already running, done, or blocked is not a candidate.
  • It has a prompt. A task with nothing to run cannot start, whatever its status says.
  • It is not on hold. Cancelling a task, or a failed attempt to spawn its session, takes it out of the queue without rewriting the plan.
  • It is not blocked. Either it was parked as blocked, or one of its dependencies failed.
  • Every dependency is met. This is the interesting one, and it is not the same as "every dependency finished".

Tasks that pass all five are sorted by their plan order, and the first ones up to the free slots start. Everything else is left where it was.

A dependency is met when its work is on the branch

This is the part that surprises people. A dependency task is not met just because it passed. It is met when its work has landed on the integration branch the run is building.

The reason is mechanical. A change task runs in its own worktree, cut from the integration branch at the moment the task starts. If the scheduler started a dependent as soon as a predecessor's marker appeared, it would cut the dependent's worktree before the predecessor's commit had been merged, and the dependent would look at a tree that does not contain the code it depends on. It would then fail for a reason that has nothing to do with its own prompt, or worse, build on the old shape of the file and pass anyway.

So the gate checks two things in sequence. The predecessor must be marked completed, and, if it ran in an isolated worktree, its record must read as merged. Between those two moments the dependent simply waits.

The same rule has a second edge. Ops tasks and manual tasks run in your own checkout, not in a worktree, so they see landed work only after a merge too. Their completed dependencies whose work has not been merged yet form a merge gate, and the task waits behind it. That is what stops a test or a deploy task from running against a checkout that does not have the change it is meant to exercise.

Order is fixed, parallelism is a cap

Plan order is not a schedule, it is a tie breaker. Once tasks pass the gates, the ready ones are taken lowest order first, and only as many as there are free slots. Independent tasks run side by side, three at a time by default. The cap is a slot count, not fixed waves, so as one task lands the next ready one moves up, with no reshuffling of the plan.

This is why two tasks that touch different files can genuinely overlap, and why two that share a file should not be declared independent. The scheduler trusts the dependency list it was given. If you leave an edge out, it will happily run both at once and let the merge step find the conflict.

A failure upstream stops the queue downstream

A failed dependency blocks its dependents. They are not started, and they are not silently dropped: they wait as blocked while you decide what to do with the failed task. Everything not downstream of the failure keeps running, which is the point of reading the graph rather than freezing the whole plan.

Re-running the failed task releases its dependents once it lands. There is no separate resume step for them, because they were never started.

You can change the graph, and the scheduler follows

Dependencies are part of the plan, so they are editable before the run and fixable inside it. One command sets the list:

$ ordewell task-deps 3 1,2
$ ordewell status
Enter fullscreen mode Exit fullscreen mode

The guard on that edit is deliberately narrow. A task cannot depend on itself, cannot name an unknown task, and cannot depend on a task that comes after it in the plan. In practice that means the graph the planner emits is acyclic by construction: edges only ever point backwards along the list, so there is no cycle for the scheduler to trip over at run time. It is the one edit that could leave a plan unschedulable, and it is the one edit that is checked for well-formedness rather than left to the model.

While a plan runs, the task list the sessions see is a report of the stored state, not a plan the model reinterprets. A running session is told which line it is on and that the rows above it are context, not work to repeat.

Honest limits

  • The list is trusted, not verified. If two independent tasks really do edit the same file, the scheduler runs them together and the conflict surfaces at merge time.
  • Parallelism is a count, not a budget. The cap keeps three sessions in flight. It does not know a long test suite from a one line edit, so slots can sit idle.
  • Blocked is sticky until you act. A dependent of a failed task will not retry on its own. Clearing it means fixing or re-running the upstream task, or editing the dependency.
  • A missing edge is invisible. The dependency graph is also the context boundary and the ordering rule, so an edge the planner forgot removes both the wait and the hand off.

Where this lives in the code

Everything above is in the repository: readiness.ts (the readiness function, the gates, the merge gate, the slot cap), the dependency guard in TaskOps.ts (canSetDependencies: no self edges, no forward edges, no unknown ids), and the task edit validator. The project is at github.com/ordewell/ordewell, and a longer version of this page sits at ordewell.ai/task-dependencies.

Top comments (0)