FlowDrop Workflow Specification 1.0-draft

What a loop is (its body, and that it is keyed by its head)

A loopback edge on a canvas draws a cycle, but the loop is the set of nodes that cycle actually turns. Nearly everything about rounds, budgets and staleness rests on this definition.

The rule

Normative: this is the rule
  1. For a loopback edge running from tail S back to head H, the loop's body is ({H} ∪ descendants(H)) ∩ ({S} ∪ ancestors(S)), computed over the forward graph alone: every edge type excluded from execution is excluded here too.
  2. The bodies of loopback edges sharing a head are unioned; a loop is keyed by its head, so two tails re-entering one node are two continuations of one loop with one round counter, and not two loops.
  3. Membership is topological, not observed: a node on a conditional arm inside the body is in the loop even on rounds it does not run, while a node downstream of the tail but not upstream of it, and a dead end that cannot reach the tail, are both outside it, since neither can influence a later round.
  4. A loopback edge whose tail is not downstream of its head closes no cycle and yields a body containing only the head, rather than being discarded.

What it means

A loopback edge is drawn from one node back to an earlier one, but the loop it closes is not "everything the loopback edge touches" — it is the intersection of what the head can still reach and what can still reach the tail, over the forward graph alone. An edge type excluded from execution (a tool wire, an agent-result wire, the loopback edge itself) is excluded from that computation too, so a tool consumer downstream of the loop never pulls the loop's body wider than it is.

Two clauses are easy to get backwards. First, a loop is keyed by its head, not by the loopback edge that closes it: two tails re-entering the same node are two continuations of one loop with one round counter, not two loops each counting its own rounds. Second, a loopback edge whose tail cannot reach its head closes no cycle at all — but that is not treated as if no loop existed; it yields a body containing only the head.

Membership is topological, not observed. A node on a conditional arm inside the body is in the loop on every round, including the rounds it does not run, because what decides membership is whether the topology lets it influence a later round — not whether this particular round happened to reach it.

Example

A gateway's two arms both loop back into the same head:

Two tails re-entering one head
[
  {"source": "head", "target": "gateway"},
  {"source": "gateway", "target": "tail_a"},
  {"source": "gateway", "target": "tail_b"},
  {"source": "tail_a", "target": "head"},
  {"source": "tail_b", "target": "head"}
]
The loop the two tails resolve tounioned
{"head": ["gateway", "head", "tail_a", "tail_b"]}

One loop, one body — not two loops sharing a head. A loopback edge whose tail cannot reach its head behaves differently again:

A loopback edge whose tail cannot reach the head
[
  {"source": "a", "target": "b"},
  {"source": "elsewhere", "target": "head"}
]
The body it resolves tohead-only
["head"]

The loop still exists — it is not discarded — its body is simply the head alone, with nothing between it and itself to re-run.

Rule identifiers are permanent and are never renumbered. Each implementation publishes its own standing against these rules; this specification does not.spec 1.0-draft · SG-16 · changed in spec 1.0