Repository navigation
A fan-out node can route to END instead of fanning out
Fixes a non-terminating loop. A node that both fans out over an array and carries a
withGoto/onError edge to END — the shape a task-driven loop takes, "fan out over
what is pending, or end the flow when there is nothing left to do" — now ends the flow
when the task steers to END.
Previously it converged on its fan-in instead: an explicit route to END and a
genuinely empty forEach array both leave the cohort empty, and the engine could not
tell them apart. Where the fan-in leads back to the source, which is the point of the
shape, the flow never terminated and wrote a step row per lap — thousands within
seconds — until something outside it gave up.
No schema migrations. No change to dispatch, scheduling, recovery, or pool sizing.
Newly expressible
g.AddTransitionForEach("Decide", "Work", "pending", "current")
g.AddTransitionGoto("Decide", workflow.END) // the task picks, per round
g.AddTransition("Work", "Join")
g.AddTransition("Join", "Decide")
g.SetFanIn("Join")Validate() used to reject this outright ("unpopped fan-out frames"), because the
goto was treated as a branch of the fan-out. It is a runtime override the task body
chooses — taking it means no branches are spawned at all — so it now carries no
fan-out frame, and reaching END through it is legal.
Behavior change: two graph shapes are now rejected at Create
Same rule, applied in the other direction. An override edge out of a fan-out node
bypasses the fan-out, so it must not rejoin it:
| Override edge out of the fan-out node | |
|---|---|
→ END |
allowed |
| → that node's own fan-in, directly | allowed (converges on the source's own state, as an empty array does) |
| → a path that never reaches the fan-in | allowed |
| → a node that later leads back into the fan-in | rejected — no fan-out frame to pop |
| → one of the fan-out's own branches | rejected — two different lineage stacks |
The two rejected shapes passed Validate() in earlier releases and then failed at
dispatch — the fan-in was waiting on branches that were never created, so the arrival
had no cohort to arrive at. The failure just moves to Create, where it is
diagnosable.
Nothing in flight is affected. A graph frozen onto a running flow is never
re-validated, so existing flows continue as they are; only new Create calls see the
check. If one of your graphs has an override edge out of a fan-out node, validate it
before upgrading.
Note that this restricts the fan-out node's own outgoing edges only. A branch
whose onError handler rejoins the fan-in is unchanged and still valid — that is the
documented "an errored branch continues down its handler path and rejoins the cohort"
behavior.