diff --git a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.Counters.cs b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.Counters.cs
index 06f3ec9df..30f959cce 100644
--- a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.Counters.cs
+++ b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.Counters.cs
@@ -313,11 +313,12 @@ public partial class Flowchart
if (flowScope.AnyInboundConnectionsFollowed(flowGraph, outboundActivity))
{
+ // This is the first inbound connection followed; schedule the outbound activity
+ var scheduleResponse = await ScheduleOutboundActivityAsync(flowchartContext, outboundActivity, completionCallback);
// An inbound connection has been followed; cancel remaining inbound activities
await CancelRemainingInboundActivitiesAsync(flowchartContext, outboundActivity);
- // This is the first inbound connection followed; schedule the outbound activity
- return await ScheduleOutboundActivityAsync(flowchartContext, outboundActivity, completionCallback);
+ return scheduleResponse;
}
if (flowScope.AllInboundConnectionsVisited(flowGraph, outboundActivity))
diff --git a/test/integration/Elsa.Activities.IntegrationTests/Flow/FlowchartCounterBasedTests.cs b/test/integration/Elsa.Activities.IntegrationTests/Flow/FlowchartCounterBasedTests.cs
index fbcc4094c..c589343af 100644
--- a/test/integration/Elsa.Activities.IntegrationTests/Flow/FlowchartCounterBasedTests.cs
+++ b/test/integration/Elsa.Activities.IntegrationTests/Flow/FlowchartCounterBasedTests.cs
@@ -1,4 +1,5 @@
using Elsa.Testing.Shared;
+using Elsa.Workflows;
using Elsa.Workflows.Activities;
using Elsa.Workflows.Activities.Flowchart.Activities;
using Elsa.Workflows.Activities.Flowchart.Models;
@@ -341,4 +342,67 @@ public class FlowchartCounterBasedTests
Assert.Equal("Connected", _output.Lines.ElementAt(0));
Assert.DoesNotContain("Unconnected", _output.Lines);
}
+
+ ///
+ /// Regression test for the ordering fix in :
+ /// the outbound activity must be scheduled before remaining inbound branches are canceled.
+ /// Previously, canceling first could trigger CompleteIfNoPendingWorkAsync while the outbound
+ /// activity was not yet in the scheduler, causing the flowchart to finish prematurely without running
+ /// the activity downstream of the join.
+ ///
+ [Fact(DisplayName = "WaitAny join schedules outbound before canceling blocked branch, preventing premature completion")]
+ public async Task WaitAnyJoin_SchedulesOutboundBeforeCancelingBlockedBranch()
+ {
+ // Arrange
+ // Flowchart structure:
+ // Start
+ // ├─► Branch1 (fast) ─► Join (WaitAny) ─► AfterJoin
+ // └─► Branch2 (blocking, creates a bookmark) ─►┘
+ //
+ // Because the scheduler is LIFO, Branch2 executes first and suspends with a bookmark.
+ // Branch1 then completes and triggers the WaitAny join.
+ // The join must schedule AfterJoin before canceling Branch2; otherwise
+ // CompleteIfNoPendingWorkAsync fires while AfterJoin is not yet queued,
+ // completing the flowchart prematurely without executing AfterJoin.
+ var start = new WriteLine("Start");
+ var branch1 = new WriteLine("Branch1");
+ var branch2 = new BlockingActivity { Id = "BlockingBranch" };
+ var join = new FlowJoin { Mode = new(FlowJoinMode.WaitAny) };
+ var afterJoin = new WriteLine("AfterJoin");
+
+ var flowchart = new Flowchart
+ {
+ Start = start,
+ Activities = { start, branch1, branch2, join, afterJoin },
+ Connections =
+ {
+ CreateConnection(start, branch1),
+ CreateConnection(start, branch2),
+ CreateConnection(branch1, join),
+ CreateConnection(branch2, join),
+ CreateConnection(join, afterJoin)
+ }
+ };
+
+ // Act
+ var result = await RunFlowchartAsync(_services, flowchart, FlowchartExecutionMode.CounterBased);
+
+ // Assert: the outbound path of the join must have executed
+ Assert.Contains("AfterJoin", _output.Lines);
+
+ // Assert: the workflow must have finished, not suspended waiting for the canceled bookmark
+ Assert.Equal(WorkflowStatus.Finished, result.WorkflowState.Status);
+
+ // Assert: the blocked branch's bookmark was cleared when it was canceled
+ Assert.Empty(result.WorkflowState.Bookmarks);
+ }
+
+ ///
+ /// A minimal activity that suspends by creating a bookmark, simulating a blocking activity
+ /// such as a Delay or Timer that would normally be resumed by an external stimulus.
+ ///
+ private sealed class BlockingActivity : Activity
+ {
+ protected override void Execute(ActivityExecutionContext context) => context.CreateBookmark();
+ }
}