Run the same subgraph many times in parallel, each instance receiving a different input, results merged back deterministically.
The "same subgraph at two-or-three call sites" pattern from
ExplicitMapping
handles cases where you know the parent fields up front. Fan-out
handles N call sites where N is determined at runtime: "for each
item in state.urls, run the scraping subgraph; collect the
results."
A fan-out can dispatch instances driven by a list in state
(items_field mode) or by a count resolved from state (count mode).
items_field mode: one instance per item in a parent list field:
from openarmature.graph import FanOutConfig, FanOutNode
scrape_all = FanOutNode(
name="scrape_all",
config=FanOutConfig(
subgraph=scrape_subgraph, # CompiledGraph[ScrapeState]
items_field="urls", # parent list field, one instance per item
item_field="url", # subgraph field that receives each item
collect_field="content", # subgraph field whose value is collected
target_field="contents", # parent list field that receives the collection
concurrency=4,
error_policy="fail_fast", # or "collect"
on_empty="raise", # or "noop"
),
)
builder.add_node("scrape_all", scrape_all)count mode: fixed-or-dynamic instance count, no list field:
fan_out = FanOutNode(
name="sample",
config=FanOutConfig(
subgraph=sample_subgraph,
count=8, # int or callable: state -> int
collect_field="reading",
target_field="readings",
concurrency=4,
),
)Both count and concurrency accept a callable that takes the
pre-fan-out parent state and returns an int (None for concurrency
means unbounded). That lets you size the dispatch from state at run
time.
Each instance gets its own subgraph state, distinct from siblings, distinct from the parent. By default the instance receives only:
- the dispatched item in the field named by
item_field(initems_fieldmode); and - the parent-field-name-mapped values declared in
inputs.
inputs is a Mapping[subgraph_field, parent_field]. The subgraph
fields not named in inputs (and not item_field) take their
schema defaults; same closed-by-default-on-the-way-in posture as
the explicit-projection story for ordinary subgraphs.
On exit, each instance's collect_field value becomes one element
of the parent's target_field list, in instance-index order. To
collect additional per-instance fields, declare
extra_outputs: Mapping[parent_field, subgraph_field]; each becomes
its own parent list of the same length, instance-index-aligned.
Two values:
"fail_fast"(default): the first instance failure cancels the in-flight siblings (asyncio.gathersemantics) and propagates as aNodeExceptionwrapping the failing instance's cause, withrecoverable_stateset to the parent's pre-fan-out snapshot. Use this when one bad result invalidates the rest."collect": instance failures are captured; the fan-out runs to completion. Failed instances contribute nothing totarget_field. If you declareerrors_fieldon the config, each failed instance produces a record ({"fan_out_index": str(idx), "category": str}) appended to that parent list field.
Choose by whether partial results are useful.
After the fan-out completes, the parent receives a partial update containing:
target_field: list ofcollect_fieldvalues, instance-index order.- Each parent name in
extra_outputs: list of values from the named subgraph field, instance-index order. count_field(if configured): the instance count.errors_field(if configured,"collect"policy only): per-instance error records.on_empty="noop"for an empty items_field → all the above with empty lists;count_fieldset to 0.
If items_field is set and the parent list is empty (or count
resolves to 0):
on_empty="raise"(default): raisesFanOutEmpty(a runtime error category).on_empty="noop": emits an empty partial (no instances dispatched, no errors).
The fan-out node's own started / completed events carry a
fan_out_config payload populated from the resolved
item_count / concurrency / error_policy / parent_node_name.
Per-instance events have fan_out_index = N (0-based) and a
namespace whose final element is the fan-out node's name; instances
do NOT contribute a separate synthetic namespace element. Backends
disambiguate per-instance spans using fan_out_index alongside the
namespace.
A fan-out node's completed event triggers a save like any other
outermost-graph or subgraph-internal node. Per-instance internal
events do NOT save in the shipping version; on resume, the
fan-out re-runs end-to-end if it hadn't completed (atomic restart).
A per-instance fan-out resume mode is planned but not yet shipped.
The fan_out_progress field on CheckpointRecord is reserved for
its eventual contents. Until it lands, atomic restart is the
shipping behavior.
The signal: N similar pieces of work, N depends on state at runtime
(not at build time), the work is independent enough to run
concurrently. If N is known at build time and small (≤3),
ExplicitMapping at multiple subgraph sites is simpler. If the
work isn't independent (instance 2 needs instance 1's output),
that's a linear pipeline, not fan-out.
- Not a map-reduce. No reduce phase beyond the parent's reducers. If you need a real reduce, do it in a node after the fan-out.
- Not a queue. All instances dispatch within a single invocation; the engine doesn't persist them.
- Not retry. If an instance fails and you want a retry,
wrap the subgraph (or individual nodes inside it) with retry
middleware. The fan-out's
error_policyis a fan-in-collection decision, not a recovery one.