Tutorial 4: Fan-out and Fan-in Edges¶
children (fan-out) and fan_in are the two edge primitives for
controlling parallel execution. They are recursive and composable, allowing
arbitrarily deep task trees.
1. Basic: fan-out — launch parallel branches¶
When a node has children, all children start simultaneously as soon as the
parent completes. This is the fundamental pattern for parallelism.
flowchart LR
D[dispatcher] --> A[agent_a]
D --> B[agent_b]
D --> C[agent_c]
models:
- llm: "ollama"
model: "qwen2.5:7b"
host: "http://localhost:11434"
prompts:
- template: # 0 — dispatcher
system_template:
role: Decompose the research question into three sub-topics.
prompt_template:
question: "{user_message}"
- template: # 1 — economic angle
system_template:
role: Analyse only the economic aspects of the topic.
prompt_template:
topic: "{user_message}"
- template: # 2 — environmental angle
system_template:
role: Analyse only the environmental aspects of the topic.
prompt_template:
topic: "{user_message}"
- template: # 3 — social angle
system_template:
role: Analyse only the social aspects of the topic.
prompt_template:
topic: "{user_message}"
nodes:
- id: "dispatcher"
model: 0
temperature: 0.2
max_tokens: 256
show: false
prompt: { template: 0, user_message: true }
- id: "economic_analyst"
model: 0
temperature: 0.5
max_tokens: 512
show: true
prompt: { template: 1, user_message: true }
- id: "environmental_analyst"
model: 0
temperature: 0.5
max_tokens: 512
show: true
prompt: { template: 2, user_message: true }
- id: "social_analyst"
model: 0
temperature: 0.5
max_tokens: 512
show: true
prompt: { template: 3, user_message: true }
edges:
- node: "dispatcher"
children:
- node: "economic_analyst"
- node: "environmental_analyst"
- node: "social_analyst"
from kegal import Compiler
with Compiler(uri="fanout.yml") as compiler:
compiler.user_message = "Impact of electric vehicles on urban areas."
compiler.compile()
outputs = compiler.get_outputs()
for node in outputs.nodes:
if node.show:
print(f"\n[{node.node_id}]")
for msg in node.response.messages or []:
print(msg)
The three analyst nodes run concurrently — the total wall-clock time is roughly the time of the slowest one, not their sum.
2. Basic: fan-in — wait for multiple branches¶
fan_in makes a node wait for every listed predecessor before it starts.
Use it to aggregate results from parallel branches.
flowchart LR
A[agent_a] --> S[synthesizer]
B[agent_b] --> S
C[agent_c] --> S
prompts:
- template: # 4 — synthesizer
system_template:
role: |
Synthesise the three analyses below into a single balanced report.
prompt_template:
findings: "{message_passing}"
nodes:
- id: "synthesizer"
model: 0
temperature: 0.4
max_tokens: 768
show: true
message_passing: { input: true }
prompt: { template: 4 }
edges:
- node: "synthesizer"
fan_in:
- node: "economic_analyst"
- node: "environmental_analyst"
- node: "social_analyst"
synthesizerwill not start until all three analysts have finished.
3. Intermediate: combined fan-out + fan-in¶
The canonical parallel pipeline: a dispatcher fans out to specialists, and a synthesiser fans in to aggregate.
flowchart LR
D[dispatcher] --> A[economic_analyst]
D --> B[environmental_analyst]
D --> C[social_analyst]
A --> S[synthesizer]
B --> S
C --> S
edges:
- node: "dispatcher"
children:
- node: "economic_analyst"
- node: "environmental_analyst"
- node: "social_analyst"
- node: "synthesizer"
fan_in:
- node: "economic_analyst"
- node: "environmental_analyst"
- node: "social_analyst"
Nodes appear twice — once in
children(who launches them) and once infan_in(who waits for them). This is correct and intentional.
Full working example with message passing:
models:
- llm: "ollama"
model: "qwen2.5:7b"
host: "http://localhost:11434"
prompts:
- template: # 0 — dispatcher
system_template:
role: Briefly state the topic for downstream analysis.
prompt_template:
topic: "{user_message}"
- template: # 1 — economic
system_template:
role: Economic analysis specialist. Be concise — 3 bullets max.
prompt_template:
topic: "{user_message}"
- template: # 2 — environmental
system_template:
role: Environmental analysis specialist. Be concise — 3 bullets max.
prompt_template:
topic: "{user_message}"
- template: # 3 — social
system_template:
role: Social analysis specialist. Be concise — 3 bullets max.
prompt_template:
topic: "{user_message}"
- template: # 4 — synthesizer
system_template:
role: |
Combine the specialist analyses below into a 2-paragraph summary.
prompt_template:
analyses: "{message_passing}"
nodes:
- id: "dispatcher"
model: 0
temperature: 0.1
max_tokens: 128
show: false
prompt: { template: 0, user_message: true }
- id: "economic_analyst"
model: 0
temperature: 0.5
max_tokens: 256
show: false
message_passing: { output: true }
prompt: { template: 1, user_message: true }
- id: "environmental_analyst"
model: 0
temperature: 0.5
max_tokens: 256
show: false
message_passing: { output: true }
prompt: { template: 2, user_message: true }
- id: "social_analyst"
model: 0
temperature: 0.5
max_tokens: 256
show: false
message_passing: { output: true }
prompt: { template: 3, user_message: true }
- id: "synthesizer"
model: 0
temperature: 0.4
max_tokens: 768
show: true
message_passing: { input: true }
prompt: { template: 4 }
edges:
- node: "dispatcher"
children:
- node: "economic_analyst"
- node: "environmental_analyst"
- node: "social_analyst"
- node: "synthesizer"
fan_in:
- node: "economic_analyst"
- node: "environmental_analyst"
- node: "social_analyst"
4. Advanced: nested fan-out¶
children is recursive — a child node can itself have children, creating
multi-level task trees.
flowchart LR
A[A] --> B[B]
A --> C[C]
B --> D[D]
B --> E[E]
edges:
- node: "A"
children:
- node: "B"
children: # B fans out to D and E after B completes
- node: "D"
- node: "E"
- node: "C"
C starts as soon as A completes. D and E start as soon as B
completes. All three (C, D, E) may run in parallel if A and B
finish before them.
5. Advanced: fan-in waiting for nested output¶
fan_in can reference nodes at any depth — including children of children.
edges:
- node: "A"
children:
- node: "B"
children:
- node: "D"
- node: "E"
- node: "C"
- node: "F"
fan_in:
- node: "C"
- node: "D"
- node: "E" # F waits for the leaf nodes, not just B
Fdoes not depend onBitself — only onDandE. IfBfinished but one of its children is still running,Fwill wait.
6. Advanced: guard + fan-out¶
A guard node automatically precedes all other nodes, including fan-out children. The guard fires first; if it fails, no child ever starts.
nodes:
- id: "input_guard"
...
structured_output:
parameters:
validation: { type: "boolean" }
required: ["validation"]
- id: "analyst_a"
...
- id: "analyst_b"
...
edges:
- node: "analyst_a" # guard is injected automatically before these
- node: "analyst_b"
Key points¶
childrenmeans fan-out: the parent completes, then children start in parallel.fan_inmeans aggregation: the node waits for all listed predecessors.- Both primitives are recursive — nest them freely.
- A node appears in
children(of who launches it) and infan_in(of who waits for it). This double appearance is correct. - Guard nodes automatically precede all non-guard nodes at their level — no explicit edge entries are needed.
message_passingandfan_in/childrenare independent: fan-out controls scheduling; message passing controls data flow.
7. Intermediate: ordered_children — sequential fan-out¶
ordered_children is the sequential counterpart of children. Siblings execute
one after another instead of in parallel:
step_1 runs after dispatcher. step_2 waits for step_1. step_3 waits
for step_2. Equivalent to manually chaining children, but flat and readable.
children and ordered_children can coexist on the same edge — the parallel
set and the sequential set are independent:
edges:
- node: "parent"
children: # A and B run in parallel
- node: "A"
- node: "B"
ordered_children: # C then D run sequentially
- node: "C"
- node: "D"
8. Intermediate: ordered_fan_in — sequential aggregation¶
ordered_fan_in makes predecessors execute sequentially before the aggregator:
edges:
- node: "synthesizer"
ordered_fan_in:
- node: "collector"
- node: "processor"
- node: "validator"
collector → processor → validator → synthesizer. Each step waits for
the previous. Useful when each predecessor builds on the output of the last.
fan_in (parallel) and ordered_fan_in (sequential) can coexist on the same
edge, acting on different sets of predecessors:
edges:
- node: "synthesizer"
fan_in: # X and Y run in parallel, both feed synthesizer
- node: "X"
- node: "Y"
ordered_fan_in: # A then B sequentially, both feed synthesizer
- node: "A"
- node: "B"
Related tutorials: 01 Message passing — passing data between nodes
03 Guard nodes — automatic pre-flight barriers
10 Blackboard — shared state across fan-out branches
12 ReAct loop — an alternative to fan-out for iterative dispatch