Loops in a DAG: modelling "repeat until it passes" without breaking acyclicity
There is an apparent contradiction at the heart of graph-based planning. The case for plans as directed acyclic graphs is airtight (one object, many views), and the acyclicity is not negotiable: work cannot depend on its own completion, and every scheduling and forecasting algorithm relies on it. Yet the most common pattern in real work is a loop. Draft, review, revise, review again. Test, fix, test. Pitch, get rejected, refine, pitch again.
Draw that honestly as a back edge (revise points back to review) and the graph stops being a DAG, the topological sort stops existing, and the engine has nothing to chew on. So most tools quietly ban the pattern, and users model loops as either one optimistic task ("revisions") or a guessed-in-advance count ("revision round 1, 2, 3"). Both are lies of different flavours: the first hides the structure, the second hard-codes precisely the thing you do not know.
The loop is a node#
The resolution is to stop modelling the loop as a cycle between tasks and start modelling it as a single structured node that contains repetition. In TOL this is the iteration:
outcome passed: boolean { default_prior: 0.5 }
iteration attempts {
max_count: 6
template: milestone attempt {
task try "Run the experiment" { agent: alice; estimate: 60min cv 0.3 }
}
over: until(passed = true)
}
Read it as: repeat the template (an attempt producing the passed outcome) until the exit condition fires, with a hard ceiling of six. The graph stays acyclic, because from the outside the iteration is one node with normal in and out edges. The repetition lives inside the construct, where the engine can reason about it, instead of in a back edge, where nothing can.
The ceiling is not an apology; it is the design. TOL is a total language: every plan provably terminates, which is what makes exhaustive simulation possible at all (why totality matters). An unbounded loop is exactly the thing a plan must not contain. max_count is the planner's admission that even persistence has a budget, and if the loop hits the ceiling, that is itself an ending worth knowing the probability of.
The loop length is a random variable#
Here is where it gets genuinely better than the workarounds. The number of times the loop runs is unknown, but it is not unknowable: if each attempt independently passes with probability p, the loop length follows a geometric distribution, and your forecast should reflect that.
Topolog's engine simulates the loop per sample: each simulated attempt draws its own pass/fail from the outcome's prior, and the loop continues until a pass or the cap. With p = 0.5 and a cap of six, the engine's measured expected loop length is 1.97 attempts (the analytic truncated-geometric value is 1.97 as well; the regression suite pins them against each other). Half of simulated futures finish in one attempt; a quarter need two; a long tail grinds toward the cap, and the completion spectrum inherits exactly that skew.
| Attempts needed (p = 0.5) | Share of futures |
|---|---|
| 1 | 50% |
| 2 | 25% |
| 3 | 12.5% |
| 4 to 6 | 12.5% combined |
Compare the two standard workarounds. "One revisions task, 3 hours" has no tail at all: the futures where it takes five rounds simply do not exist in the model, so the forecast is structurally optimistic. "Three fixed rounds" is wrong in both directions at once: it pays for round three in the 75% of futures that did not need it and still denies rounds four to six to the futures that did. The geometric model is not a refinement; it is the only one of the three that contains the actual phenomenon.
Iterations earn their keep operationally too. Fixed-count repetitions (run the workshop in five regions) unfold into independent instances you can pick up and complete separately on the execute board, and an until-loop spawns its next instance only when the previous one finishes without the exit condition firing, so the live plan always shows the attempts that actually exist rather than six hypothetical rows.
Loops are where uncertainty concentrates#
A closing observation from watching real plans: when a forecast has a wide, scary tail, the cause is rarely a mis-estimated task. It is almost always a loop, because a loop multiplies a whole sub-plan's duration by a random count, and that product dominates everything additive around it (spread mechanics). Which makes the modelling decision self-reinforcing: the constructs most worth getting right are exactly the ones the flat task list cannot express.
So the contradiction dissolves cleanly. Plans must be acyclic; work loops; the loop becomes a typed, bounded, probabilistic node, and the DAG keeps every guarantee while the forecast finally tells the truth about persistence: how many tries it will probably take, and how likely you are to run out of tries before you run out of problem.