doc/adr/0007-adoption-of-explicit-merge-modes-for-flowchart-joins.md
Date: 2025-09-30
Accepted
The Flowchart activity serves as a container for orchestrating workflows through activities connected via directed edges, using a token-based model for control flow. Initially, the execution logic relied on a combination of counter-based and token-based approaches, with implicit handling for merging paths (joins). However, this led to inconsistencies:
The root issue was the lack of explicit, configurable semantics for joins, relying instead on heuristics (e.g., inbound connection count >1). This made behavior opaque and error-prone, especially in unstructured flowcharts. Inspired by BPMN gateway semantics (e.g., AND-join for strict sync, OR-join for partial), we needed a clearer model to balance safety (blocking on required paths) and flexibility (proceeding on dead paths).
We refine the Flowchart's token-based execution logic (OnChildCompletedTokenBasedLogicAsync) to use an explicit MergeMode enum on activities. This eliminates null/default fallbacks, making behaviors self-documenting and configurable via activity properties.
MergeMode Enum Definition (in MergeMode.cs):
namespace Elsa.Workflows.Activities.Flowchart.Models;
/// <summary>
/// Specifies the strategy for handling multiple inbound execution paths in a workflow.
/// Uses flow-based terminology to describe merge behavior.
/// </summary>
public enum MergeMode
{
/// <summary>
/// Flows freely when possible, ignoring dead/untaken paths.
/// Opportunistic execution based on upstream completion.
/// </summary>
Stream,
/// <summary>
/// Merges only the activated/flowing inbound branches.
/// Waits for all branches that received tokens, ignoring unactivated ones.
/// </summary>
Merge,
/// <summary>
/// Converges all inbound paths, requiring every connection to execute.
/// Strictest mode - will block on dead/untaken paths.
/// </summary>
Converge,
/// <summary>
/// Cascades execution for each arriving token independently.
/// Allows multiple concurrent executions (one per arriving token).
/// </summary>
Cascade,
/// <summary>
/// Races inbound branches, executing on first arrival and blocking others.
/// </summary>
Race
}
Key Changes in Flowchart Execution:
MergeMode (via GetMergeModeAsync). Handle each mode explicitly in a switch statement.FlowGraph for forward inbound connections (acyclic); backward connections (e.g., loops) are handled naturally without inflating counts.Implementation Snippet (from Flowchart partial class; full code in PR):
switch (mergeMode)
{
case MergeMode.Cascade:
case MergeMode.Race:
// Schedule on arrival; for Race, block/cancel others.
// ...
break;
case MergeMode.Merge:
// Wait for tokens from all forward inbound connections (activated branches only).
var inboundConnections = flowGraph.GetForwardInboundConnections(targetActivity);
if (inboundConnections.Count > 1)
{
var hasAllTokens = inboundConnections.All(inbound => /* token check */);
if (hasAllTokens) await flowContext.ScheduleActivityAsync(...);
}
else
{
await flowContext.ScheduleActivityAsync(...);
}
break;
case MergeMode.Converge:
// Strictest mode: Wait for tokens from ALL inbound connections (forward + backward).
var allInboundConnections = flowGraph.GetInboundConnections(targetActivity);
if (allInboundConnections.Count > 1)
{
var hasAllTokens = allInboundConnections.All(inbound => /* token check */);
if (hasAllTokens) await flowContext.ScheduleActivityAsync(...);
}
else
{
await flowContext.ScheduleActivityAsync(...);
}
break;
case MergeMode.Stream:
default:
// Flows freely - approximation that proceeds when upstream completes.
var inboundConnections = flowGraph.GetForwardInboundConnections(targetActivity);
var hasUnconsumed = inboundConnections.Any(inbound => /* source token check */);
if (!hasUnconsumed) await flowContext.ScheduleActivityAsync(...);
break;
}
Flowchart execution starts with scheduling the root/start activity. As activities complete:
MergeMode.This ensures acyclic forward flow with support for backward loops, using tokens to track control without global state beyond the list.
Each mode defines how tokens from multiple inbounds are synchronized using flow-based terminology:
Stream (Flexible/Opportunistic Flow):
Merge (Activated Branches Synchronization):
Converge (Strictest - All Paths Required):
Cascade (Per-Token Execution):
Race (First-Wins):
Positive:
Negative: