Parallel branches¶
Source
https://github.com/LunarCommand/openarmature-python/blob/main/examples/parallel-branches/main.py
Enrich a lunar-mission news article with several independent analyses (one-sentence summary, sentiment label, topic tags, reading-time estimate, optional translation) running concurrently: some as heterogeneous subgraphs, one as a lightweight inline function, and one gated by a condition.
Overview¶
Where fan-out (the fan-out-with-retry example) runs N copies of one subgraph against different inputs, parallel-branches runs M heterogeneous branches against the same input. Different state schemas, different middleware, different topologies per branch, one dispatch.
The article goes into several branches in parallel:
- summary: bare subgraph, one node, writes
summaryback. - sentiment: subgraph wrapped in
RetryMiddleware(the classification call is short and cheap to retry), writes alabelback into the parent'ssentimentfield. - topics: bare subgraph, writes a
tagslist back into the parent'stopicsfield. - reading_time: an inline
callbranch, not a subgraph. A plain async function over the parent state estimates reading time from the word count (no LLM, no projection) and returns the parent field directly. - translation: a
callbranch gated by awhenpredicate. It runs only whentarget_languageis set on the state; with it unset (the default) the branch is skipped entirely.
The branches don't depend on each other, so they fire concurrently and the parent fans in once the dispatched branches complete.
What it teaches¶
add_parallel_branches_node: M namedBranchSpecs under one node. A branch gives its work as either a compiledsubgraph(with per-branch input/output projection) or an inlinecall, plus optional per-branch middleware and an optionalwhenpredicate.- Callable branches.
reading_timeisBranchSpec(call=fn): an inline async function over the parent state that returns parent fields directly, with no subgraph, state schema, or projection. Reach forcallwhen a leg is really just "run this one function" rather than a whole pipeline. - Conditional
when.translationcarrieswhen=lambda s: bool(s.target_language)and is skipped at dispatch unless a target language is requested: no dispatch, no contribution, no observer events. The other branches always run. - Branches with different state schemas. The summary subgraph's
state has a
summaryfield; the sentiment subgraph's haslabel; the topics subgraph's has atagslist. The projection mappings translate between the branch's vocabulary and the parent's. Callable branches need no projection: they read and write parent fields directly. - Heterogeneous per-branch middleware. The sentiment branch wraps its subgraph in retry; the others run bare. A production pipeline often wants different retry policies, timing windows, or custom middleware per branch.
- Branch insertion order = fan-in order. When two branches write to
the same parent field, the parent's reducer applies them in the
order they were declared in the
branchesmapping (not in completion order). The branches here write disjoint parent fields, so the order doesn't affect the result, but the property holds. - A
branch_attribution_observerreadsNodeEvent.branch_name. It is populated for every event from inside a branch: the inner nodes of a subgraph branch and the single branch-unit event of a callable branch. Outer-graph nodes carrybranch_name=None, and a skippedwhenbranch emits no events at all. This is the per-event attribution that lets observability backends route metrics and spans by branch.
How to run¶
The article is baked into the example. Set target_language on the
input state to also run the translation branch.
The graph¶
flowchart TD
start([start])
receive[receive]
present[present]
stop([end])
subgraph enrich [enrich: parallel-branches]
direction TB
summary[summary branch<br/>subgraph]
sentiment[sentiment branch<br/>subgraph + retry]
topics[topics branch<br/>subgraph]
reading_time[reading_time branch<br/>callable]
translation[translation branch<br/>callable + when]
end
start --> receive --> enrich --> present --> stop
enrich is the parallel-branches node; the branches inside the box
dispatch concurrently against the same article field on parent state.
The summary, sentiment, and topics branches are subgraphs; reading_time
and translation are inline callables. The translation branch is gated by
when and is skipped in the default run.
Reading the output¶
========================================================================
Lunar-mission article enrichment; independent analyses in parallel
========================================================================
Article (642 chars):
NASA's Artemis II crew capsule Integrity splashed down in the Pacific
Ocean this evening, ending a ten-day flight that carried four astronauts
on a free-return trajectory around the Moon and back...
[observer] (branch=summary) node 'write_summary' started
[observer] (branch=sentiment) node 'classify_sentiment' started
[observer] (branch=topics) node 'extract_topics' started
[observer] (branch=reading_time) node 'reading_time' started
========================================================================
Enrichment results
========================================================================
summary: <one-sentence summary>
sentiment: positive
topics: ['Artemis II', 'splashdown', 'lunar program']
reading time: 36s
translation: (skipped by `when`; set target_language to enable)
wall-clock: 1142.6 ms
The branches ran in parallel; wall-clock is closer to the slowest
single branch than to the sum of them all...
- The observer lines fire close together (often within a few ms of
each other), confirming the branches dispatched in parallel rather
than serially. There is no
translationline: itswhenpredicate was false, so the branch was skipped and emitted nothing. - The reading_time line comes from a callable branch. It carries
branch_nameexactly like the subgraph branches' inner nodes, so per-branch attribution is uniform across both branch forms. branch_nameattribution is what makes per-branch observability tractable.write_summaryknows nothing aboutbranch_name; it's the engine that tags the event for the observer.- Wall-clock under 1500 ms for several LLM branches is the clearest indicator of parallelism. Run serially at roughly 1s each they would sum to several seconds; under parallel dispatch the wall-clock approaches the slowest branch's duration. The reading_time branch is effectively free (no LLM).
- Disjoint output fields mean the reducer order at fan-in doesn't
matter here. If two branches both wrote to
summary, the declared branch order would determine which value won under the defaultlast_write_winsreducer. ```