Supporting Multi-Upstream Fan-In and State Aggregation in Workflow Engines
How TenseAI handles parallel branch convergence and state envelope aggregation in DAG workflow execution.

Fan-out is the easy half of graph execution: one node triggers several parallel downstream nodes, and each of those branches runs independently. Most workflow engines get this right without much trouble.
Fan-in is where things get harder. That's the pattern where multiple parallel branches need to converge into a single downstream node — and that node needs the combined output of all of them before it can run.
A Concrete Example
Take an automated lead-qualification workflow:
Start Node -> Branch A: Apollo Lead Search ----\
Start Node -> Branch B: Apify Web Scraper ----> Join Node C (Multi-Upstream Fan-In)
-> Aggregated Context Envelope
-> Google Sheets Append RowBranch A queries Apollo for contact details. Branch B queries Apify to scrape company website data. Both run in parallel — there's no reason to make one wait on the other. But the Google Sheets node that writes the final row needs both results merged into a single, coherent record before it can execute. Write too early, using only Branch A's data, and you've silently dropped the company research half of the row.
Solving Fan-In in TenseAI
TenseAI handles this with three coordinated engine components.
1. Join Node Identification at Compile Time
During graph compilation (graph_compiler.py), the compiler inspects every node's in-degree — the number of incoming edges — and explicitly flags any node with more than one upstream dependency as a join node:
join_nodes = [_node_summary(node) for node in compiled_nodes if len(node["incoming"]) > 1]This isn't a runtime discovery — it's known and validated before a single tool call happens, as part of the same Kahn's-algorithm compilation pass that produces the execution order.
2. Explicit Dependency Tracking in the Plan Builder
Once a node is flagged as a join, graph_plan_builder.py records exactly which upstream steps it depends on: dependsOnStepIds: ["step_apollo", "step_apify"]. The execution plan doesn't just know that a node has multiple parents — it knows precisely which step IDs it needs to wait for before it's eligible to run.
3. Context Aggregation at Execution Time
When the runner (runner.py) reaches a join node, it doesn't just pick one upstream envelope and discard the rest. It merges the data envelopes of all upstream dependencies into a single, unified WorkflowContextEnvelope. Contacts contributed by Branch A and items contributed by Branch B are combined without collision — each canonical field (contacts, items, rows, and so on) accumulates contributions from every branch that produced one.
The join node then executes with full context: it can see everything every upstream branch produced, correctly merged, regardless of how many branches fed into it.
Why This Matters
Without proper fan-in support, workflow authors are forced to either serialize branches that should run in parallel — losing the performance and clarity benefits of a real DAG — or accept incomplete data at the join point. Neither is acceptable for production automation.
By handling join detection at compile time, dependency tracking in the plan, and envelope aggregation at execution time, TenseAI lets workflow designers build genuinely parallel pipelines — multiple lead sources, multiple enrichment steps, multiple research branches — and trust that everything converges correctly before the next action fires.
Next up: none of this matters if a crash mid-run causes duplicate side effects. That's where idempotency and stale-run reconciliation come in.
Start Automating with TenseAI
Build production-grade, multi-tool workflows with Kahn's DAG compilation and Universal Output Envelopes.