What
A Fan-out block that runs several branches at once and joins their results — the rigid, declared version of the loose multi-edge fan-out we do not support (and should not).
Shape:
- The block declares how many outputs it has. The user sets that count; the block shows exactly that many source handles. Nothing is inferred from whatever the user happened to connect.
- Each handle starts one branch. All branches run in parallel from the same input.
- When every branch has finished, the block hands the user a transformation script whose
input is an array, where input[0] is branch 0's result, input[1] is branch 1's, and so on.
- The branch order is explicit and set by the user — see the ordering menu below. The array index is the contract the user's script is written against, so it is never inferred.
This is deliberately more rigid than free-form fan-out: a fixed, ordered, declared set of branches with one join point, rather than "any handle may have any number of edges".
The ordering menu (UI)
The order of the branches is the user's decision, made in one place, and visible without running anything. Fluxify shows the mapping instead of hiding it — less magic and less black box than other tools, where "which parallel result is which" is something you find out by trial and error.
- The fan-out block's settings panel has an ordering menu: a compact vertical list, one row per branch, reorderable by drag and drop — a Trello list, but condensed to a single tight column, not a card board.
- Each row states its index and the block that will feed it, in plain words:
input[0] → Fetch User, input[1] → Fetch Orders. Reading the list is reading the array the script receives.
- Hovering a row highlights the connected block on the canvas (and its branch's edge), so the user can see at a glance which downstream block occupies position
i. Hovering a block can highlight its row the same way.
- Dragging a row to a new position immediately renumbers the rows, so the user sees the new
input[i] → block mapping before saving — no guessing what the reorder did.
- A branch whose handle is not connected yet still gets a row, marked as unconnected, so the indices never silently shift when a connection is added later.
- Keep it concise: index, block name, drag handle. No card previews, no nested settings — the row's job is to make one mapping obvious.
Is it possible with the current compiler?
Yes. Checked against packages/blocks/compiler.ts:
- Every block is already emitted as
async function $block_N($state, $input, $end) returning a promise, so branches are independently awaitable. The emitter for the fan-out block can be roughly await Promise.all([...]) over each handle's continuation.
edgeTo() currently throws "multi-edge fan-out is not supported yet" (compiler.ts:277) — an explicit placeholder, not an architectural limit. The fan-out block would be the one block type allowed past it, on its own declared handles only.
- There is already precedent for a continuation that returns a value instead of ending the request: loop bodies are emitted via
node.body(handle, initExpr) with $endBody = () => undefined. Fan-out needs the same with $endBranch = (output) => output, so a branch yields its final value into the join array instead of terminating the route.
What it still has to solve
- Edge order has to be persisted. The
edges table has no ordering column, and buildEdgeMap just keeps whatever row order came back from the query — so today there is nothing for the ordering menu to save. The order the user sets needs a real home: an order column on edges, or the index encoded in the handle (source-0, source-1). Without it, input[2] means a different branch after a reload, and the menu above cannot be truthful.
vars is shared across branches. Branches interleave at every await, so two branches doing setvar on the same variable race. Either give each branch its own scope for the duration, or document that shared variable writes inside fan-out branches are unsupported and reject them in validation.
- Terminal blocks inside a branch. A
response block in a branch would end the whole request from inside a parallel arm. Validation should reject response (and anything else terminal) inside a fan-out branch — the join is the only exit.
- Failure policy.
Promise.all rejects on the first failure and leaves the siblings running as unhandled rejections. Decide and implement one behaviour: fail the whole route on any branch error, or use allSettled and pass per-branch outcomes into the transformation script's input.
Tracing needs no change — spans are already recorded per block with their own start/end, so parallel branches record correctly (the timeline UI may need to stop assuming a single linear order).
Related: #236 (the block builder currently produces unsupported free-form fan-out).
What
A Fan-out block that runs several branches at once and joins their results — the rigid, declared version of the loose multi-edge fan-out we do not support (and should not).
Shape:
inputis an array, whereinput[0]is branch 0's result,input[1]is branch 1's, and so on.This is deliberately more rigid than free-form fan-out: a fixed, ordered, declared set of branches with one join point, rather than "any handle may have any number of edges".
The ordering menu (UI)
The order of the branches is the user's decision, made in one place, and visible without running anything. Fluxify shows the mapping instead of hiding it — less magic and less black box than other tools, where "which parallel result is which" is something you find out by trial and error.
input[0] → Fetch User,input[1] → Fetch Orders. Reading the list is reading the array the script receives.i. Hovering a block can highlight its row the same way.input[i] → blockmapping before saving — no guessing what the reorder did.Is it possible with the current compiler?
Yes. Checked against
packages/blocks/compiler.ts:async function $block_N($state, $input, $end)returning a promise, so branches are independently awaitable. The emitter for the fan-out block can be roughlyawait Promise.all([...])over each handle's continuation.edgeTo()currently throws"multi-edge fan-out is not supported yet"(compiler.ts:277) — an explicit placeholder, not an architectural limit. The fan-out block would be the one block type allowed past it, on its own declared handles only.node.body(handle, initExpr)with$endBody = () => undefined. Fan-out needs the same with$endBranch = (output) => output, so a branch yields its final value into the join array instead of terminating the route.What it still has to solve
edgestable has no ordering column, andbuildEdgeMapjust keeps whatever row order came back from the query — so today there is nothing for the ordering menu to save. The order the user sets needs a real home: anordercolumn on edges, or the index encoded in the handle (source-0,source-1). Without it,input[2]means a different branch after a reload, and the menu above cannot be truthful.varsis shared across branches. Branches interleave at everyawait, so two branches doingsetvaron the same variable race. Either give each branch its own scope for the duration, or document that shared variable writes inside fan-out branches are unsupported and reject them in validation.responseblock in a branch would end the whole request from inside a parallel arm. Validation should rejectresponse(and anything else terminal) inside a fan-out branch — the join is the only exit.Promise.allrejects on the first failure and leaves the siblings running as unhandled rejections. Decide and implement one behaviour: fail the whole route on any branch error, or useallSettledand pass per-branch outcomes into the transformation script's input.Tracing needs no change — spans are already recorded per block with their own start/end, so parallel branches record correctly (the timeline UI may need to stop assuming a single linear order).
Related: #236 (the block builder currently produces unsupported free-form fan-out).