Refactor flowchart token handling and enhance merge mode behavior (#6937)
* Refactor flowchart token handling and enhance merge mode behavior - Improve token emission, consumption, and scheduling logic. - Add support for distinct merge modes: None, Converge, Stream, and Race. - Update `MergeMode` enum documentation to clarify behavior. - Adjust default merge mode from `Converge` to `None`. * Add integration tests for implicit join behaviors with None and Converge merge modes - Added workflows `fork-decision-join-none.json` and `fork-decision-join-converge.json` to test scenarios. - Implemented `ForkDecisionJoinTests` to validate execution logic based on merge modes. - Updated project file to include new workflows for testing. * Refactor `ImplicitJoins` tests to `JoinBehaviors` and add test for `WaitAll` join mode - Renamed `ImplicitJoins` test namespace and workflows to `JoinBehaviors`. - Added `fork-decision-join-waitall.json` workflow to test the `WaitAll` merge mode. - Refactored `ForkDecisionJoinTests` with reusable logic for execution and assertions. - Updated project file to include the new workflow for testing. * Refactor flowchart token handling for improved clarity and efficiency - Simplified token consumption and filtering logic. - Removed default port fallback for active outbound connections. - Improved readability and maintainability of token handling in merge mode scenarios. * Refactor tests and workflows for `JoinBehaviors` - Transitioned connections to inline object initializers for simplicity. - Updated workflow paths in `ParallelJoinCompletesTests` and `JoinRunsOnceTests` to match `JoinBehaviors`. - Adjusted connection definitions in `ImplicitLoopWorkflow` for consistency. * Format JSON workflow files * Add ADR for explicit merge modes in flowchart joins - Introduced `MergeMode` enum with modes: None, Converge, Stream, and Race. - Documented motivation, decision, and implementation details. - Updated solution to include new ADR file.
This commit is contained in:
parent
425e8fed3d
commit
eb7a76a8c0
1
Elsa.sln
1
Elsa.sln
|
|
@ -224,6 +224,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "adr", "adr", "{0A04B1FD-06C
|
|||
doc\adr\toc.md = doc\adr\toc.md
|
||||
doc\adr\0005-activity-execution-snapshots.md = doc\adr\0004-activity-execution-snapshots.md
|
||||
doc\adr\0006-tenant-deleted-event.md = doc\adr\0005-tenant-deleted-event.md
|
||||
doc\adr\0006-adoption-of-explicit-merge-modes-for-flowchart-joins.md = doc\adr\0006-adoption-of-explicit-merge-modes-for-flowchart-joins.md
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "bounty", "bounty", "{9B80A705-2E31-4012-964A-83963DCDB384}"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,170 @@
|
|||
# 6. Adoption of Explicit Merge Modes for Flowchart Joins
|
||||
|
||||
Date: 2025-09-30
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
|
||||
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:
|
||||
|
||||
- **Premature Scheduling in Forks**: In conditional forks with converges, untaken branches (e.g., false decision outcomes) allowed downstream activities to execute unexpectedly, violating expected blocking behavior.
|
||||
- **Stalling in Loops**: Strict token checks for all inbounds broke loops by consuming entry tokens and failing to reschedule on backward connections.
|
||||
- **Inconsistent Merges in Complex Flows**: In workflows with switches and multiple branches (e.g., MatchAny modes), dead paths (untaken defaults) caused hangs under strict rules but proceeded under approximations, leading to conflicting expectations.
|
||||
|
||||
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).
|
||||
|
||||
## Decision
|
||||
|
||||
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`):
|
||||
```csharp
|
||||
namespace Elsa.Workflows.Activities.Flowchart.Models;
|
||||
|
||||
public enum MergeMode
|
||||
{
|
||||
/// <summary>
|
||||
/// No special merging; use approximation that proceeds after all upstream sources complete, ignoring dead paths.
|
||||
/// Suitable for flexible, unstructured merges where optional branches shouldn't block.
|
||||
/// </summary>
|
||||
None,
|
||||
|
||||
/// <summary>
|
||||
/// Strict wait for tokens from all forward inbound connections. Blocks on dead/untaken paths.
|
||||
/// Use for required synchronization points.
|
||||
/// </summary>
|
||||
Converge,
|
||||
|
||||
/// <summary>
|
||||
/// Schedule on each arriving token, allowing multiple executions if supported.
|
||||
/// </summary>
|
||||
Stream,
|
||||
|
||||
/// <summary>
|
||||
/// Schedule on the first arriving token, block or cancel others.
|
||||
/// </summary>
|
||||
Race
|
||||
}
|
||||
```
|
||||
|
||||
- **Key Changes in Flowchart Execution**:
|
||||
- **Token Emission and Consumption**: On activity completion, emit tokens only for active outcomes (matching connections). Consume inbound tokens post-execution.
|
||||
- **Scheduling Logic**: For each outbound connection, evaluate the target's `MergeMode` (via `GetMergeModeAsync`). Handle each mode explicitly in a switch statement.
|
||||
- **Graph Reliance**: Use `FlowGraph` for forward inbound connections (acyclic); backward connections (e.g., loops) are handled naturally without inflating counts.
|
||||
- **Dead Path Handling**: Varies by mode (strict blocking in Converge; approximation in None).
|
||||
- **Loop Support**: Converge mode checks inbound count >1 to schedule immediately for sequentials/loops (<=1 forwards).
|
||||
- **Cancellation and Purging**: Retained for races and overall cleanup.
|
||||
|
||||
- **Implementation Snippet** (from `Flowchart` partial class; full code in PR):
|
||||
```csharp
|
||||
switch (mergeMode)
|
||||
{
|
||||
case MergeMode.Stream:
|
||||
case MergeMode.Race:
|
||||
// Existing logic: Schedule on arrival, block others for Race.
|
||||
// ...
|
||||
break;
|
||||
|
||||
case MergeMode.Converge:
|
||||
// Strict check: Wait for all forward inbounds if >1; else schedule immediately.
|
||||
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.None:
|
||||
default:
|
||||
// Approximation: Schedule if no unconsumed tokens to inbound sources.
|
||||
var inboundConnections = flowGraph.GetForwardInboundConnections(targetActivity);
|
||||
var hasUnconsumed = inboundConnections.Any(inbound => /* source token check */);
|
||||
if (!hasUnconsumed) await flowContext.ScheduleActivityAsync(...);
|
||||
break;
|
||||
}
|
||||
```
|
||||
|
||||
### Functional Overview
|
||||
Flowchart execution starts with scheduling the root/start activity. As activities complete:
|
||||
1. Emit tokens for matching outbound connections.
|
||||
2. Consume the activity's inbound tokens.
|
||||
3. For each emitted token's target:
|
||||
- Fetch its `MergeMode`.
|
||||
- Apply mode-specific logic to decide scheduling.
|
||||
4. Purge consumed tokens and complete the flowchart if no pending work.
|
||||
|
||||
This ensures acyclic forward flow with support for backward loops, using tokens to track control without global state beyond the list.
|
||||
|
||||
### Merge Modes Explained
|
||||
Each mode defines how tokens from multiple inbounds are synchronized:
|
||||
|
||||
- **None (Default/Flexible Merge)**:
|
||||
- **Behavior**: Schedules if there are no unconsumed tokens *to the sources* of inbounds (i.e., all upstream activities have completed, treating dead paths as "done").
|
||||
- **When to Use**: Flexible merges in unstructured flows; optional/exclusive branches (e.g., switch defaults) shouldn't block.
|
||||
- **Scenarios**:
|
||||
- **Forks with Untaken Paths**: Proceeds after active branches (e.g., in complex switch with dangling default).
|
||||
- **Loops**: Schedules on loop-back tokens (backward ignored in forward inbounds).
|
||||
- **Dead Paths**: Ignores untaken outcomes; no blocking.
|
||||
- **Example**: In a switch with MatchAny, untaken default doesn't hang the merge.
|
||||
|
||||
- **Converge (Strict Synchronization)**:
|
||||
- **Behavior**: Requires unconsumed, non-blocked tokens from *all* forward inbounds. For <=1 forward, schedules immediately (loop/sequential friendly).
|
||||
- **When to Use**: Required "all must happen" joins; block if any branch untaken.
|
||||
- **Scenarios**:
|
||||
- **Conditional Forks**: Blocks downstream if e.g., decision returns false and subsequent activities are connected to the true branch.
|
||||
- **Loops**: Works if forward inbounds <=1; reschedules on backward tokens.
|
||||
- **Dead Paths**: Blocks (desired for safety).
|
||||
- **Example**: Converge after parallel approvals—only proceed if all complete.
|
||||
|
||||
- **Stream (Per-Token Execution)**:
|
||||
- **Behavior**: Schedules on each arriving token; may allow multiple concurrent executions of the target.
|
||||
- **When to Use**: Streaming merges where each branch triggers independently (e.g., event streams).
|
||||
- **Scenarios**:
|
||||
- **Forks**: Executes target per branch.
|
||||
- **Loops**: Executes per iteration.
|
||||
- **Dead Paths**: Ignores; only active tokens trigger.
|
||||
- **Example**: Logging each branch outcome separately.
|
||||
|
||||
- **Race (First-Wins)**:
|
||||
- **Behavior**: Schedules on first token; blocks/cancels others (e.g., via blocked tokens and ancestor cancellation).
|
||||
- **When to Use**: Racing conditions (e.g., first response wins).
|
||||
- **Scenarios**:
|
||||
- **Forks**: Only first branch proceeds.
|
||||
- **Loops**: May race iterations if concurrent.
|
||||
- **Dead Paths**: First active wins; others blocked.
|
||||
- **Example**: Waiting for fastest API response; cancel slower ones.
|
||||
|
||||
### Handling Common Scenarios
|
||||
|
||||
- **Simple Sequential**: Any mode schedules on token arrival (single inbound).
|
||||
- **Fork-Join with Condition**: Converge blocks on false; None proceeds.
|
||||
- **Looping Construct**: All modes work; Converge uses count check to avoid strictness.
|
||||
- **Switch with Dangling Branches**: None ignores untaken; Converge blocks if required.
|
||||
- **BPMN Alignment**: None ≈ XOR/OR-join (flexible); Converge ≈ AND-join (strict); Race ≈ Event-based; Stream ≈ partial OR.
|
||||
|
||||
## Consequences
|
||||
|
||||
- **Positive**:
|
||||
- Clearer semantics: Explicit modes reduce bugs and improve workflow design.
|
||||
- Flexibility: Users choose behavior per activity.
|
||||
- Reliability: Fixes identified flaws across forks, loops, and complexes.
|
||||
- Extensibility: Enum can grow (e.g., for BPMN Complex).
|
||||
|
||||
- **Negative**:
|
||||
- Complexity: More modes mean more testing; document well.
|
||||
- Performance: Token checks add minor overhead (optimize with caching).
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
- **Heuristics-Only**: Relied on inbound count/graph structure—too brittle, led to conflicts.
|
||||
- **Full BPMN Gateways**: Dedicated activities per type (e.g., ParallelGateway)—overkill for Elsa's simplicity; would require major refactor.
|
||||
- **Dead Path Propagation**: Emit blocked tokens on untaken paths—adds complexity; deferred for future if needed (e.g., for OR-join).
|
||||
- **Counter-Based Fallback**: Retained old logic—deprecated for token purity.
|
||||
|
|
@ -121,7 +121,7 @@ public static class ActivityExtensions
|
|||
|
||||
public static MergeMode GetMergeMode(this JsonObject activity)
|
||||
{
|
||||
return activity.GetProperty<MergeMode?>("customProperties", "mergeMode") ?? MergeMode.Converge;
|
||||
return activity.GetProperty<MergeMode?>("customProperties", "mergeMode") ?? MergeMode.None;
|
||||
}
|
||||
|
||||
public static void SetMergeMode(this JsonObject activity, MergeMode? value)
|
||||
|
|
|
|||
|
|
@ -6,17 +6,24 @@ namespace Elsa.Api.Client.Shared.Enums;
|
|||
public enum MergeMode
|
||||
{
|
||||
/// <summary>
|
||||
/// Wait for all inbound paths before proceeding.
|
||||
/// No special merging; use approximation that proceeds after all upstream sources complete, ignoring dead paths.
|
||||
/// Suitable for flexible, unstructured merges where optional branches shouldn't block.
|
||||
/// </summary>
|
||||
None,
|
||||
|
||||
/// <summary>
|
||||
/// Strict wait for tokens from all forward inbound connections. Blocks on dead/untaken paths.
|
||||
/// Use for required synchronization points.
|
||||
/// </summary>
|
||||
Converge,
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Proceed when any one inbound path completes; cancel all others.
|
||||
/// Schedule on each arriving token, allowing multiple executions if supported.
|
||||
/// </summary>
|
||||
Race,
|
||||
|
||||
Stream,
|
||||
|
||||
/// <summary>
|
||||
/// Proceed when any one inbound path completes; do not cancel others.
|
||||
/// Schedule on the first arriving token, block or cancel others.
|
||||
/// </summary>
|
||||
Stream
|
||||
Race
|
||||
}
|
||||
|
|
@ -15,7 +15,7 @@ public partial class Flowchart
|
|||
var completedActivity = ctx.ChildContext.Activity;
|
||||
var flowGraph = flowContext.GetFlowGraph();
|
||||
|
||||
// Emit tokens.
|
||||
// Emit tokens for active outcomes.
|
||||
var outcomes = (ctx.Result as Outcomes ?? Outcomes.Default).Names;
|
||||
var outboundConnections = flowGraph.GetOutboundConnections(completedActivity);
|
||||
var activeOutboundConnections = outboundConnections.Where(x => outcomes.Contains(x.Source.Port)).Distinct().ToList();
|
||||
|
|
@ -24,70 +24,102 @@ public partial class Flowchart
|
|||
foreach (var connection in activeOutboundConnections)
|
||||
tokens.Add(Token.Create(connection.Source.Activity, connection.Target.Activity, connection.Source.Port));
|
||||
|
||||
// Consume tokens.
|
||||
// Consume inbound tokens to the completed activity.
|
||||
var inboundTokens = tokens.Where(t => t.ToActivityId == completedActivity.Id && t is { Consumed: false, Blocked: false }).ToList();
|
||||
foreach (var t in inboundTokens)
|
||||
t.Consume();
|
||||
|
||||
// Schedule next activities.
|
||||
// Schedule next activities based on merge modes.
|
||||
foreach (var connection in activeOutboundConnections)
|
||||
{
|
||||
var targetActivity = connection.Target.Activity;
|
||||
var mergeMode = await targetActivity.GetMergeModeAsync(ctx.ChildContext);
|
||||
|
||||
if (mergeMode is MergeMode.Stream or MergeMode.Race)
|
||||
switch (mergeMode)
|
||||
{
|
||||
if (mergeMode == MergeMode.Race)
|
||||
await flowContext.CancelInboundAncestorsAsync(targetActivity);
|
||||
case MergeMode.Stream:
|
||||
case MergeMode.Race:
|
||||
if (mergeMode == MergeMode.Race)
|
||||
await flowContext.CancelInboundAncestorsAsync(targetActivity);
|
||||
|
||||
// Check if there is any blocking token preventing the activity from being scheduled.
|
||||
var existingBlockedToken = tokens.FirstOrDefault(t => t.ToActivityId == targetActivity.Id && t.FromActivityId == connection.Source.Activity.Id && t.Outcome == connection.Source.Port && t.Blocked);
|
||||
// Check for existing blocked token on this specific connection.
|
||||
var existingBlockedToken = tokens.FirstOrDefault(t =>
|
||||
t.ToActivityId == targetActivity.Id &&
|
||||
t.FromActivityId == connection.Source.Activity.Id &&
|
||||
t.Outcome == connection.Source.Port &&
|
||||
t.Blocked);
|
||||
|
||||
if (existingBlockedToken == null)
|
||||
{
|
||||
// Schedule the target activity.
|
||||
await flowContext.ScheduleActivityAsync(targetActivity, OnChildCompletedTokenBasedLogicAsync);
|
||||
|
||||
// And block other inbound connections.
|
||||
var otherInboundConnections = flowGraph.GetForwardInboundConnections(targetActivity).Where(x => x.Source.Activity != completedActivity).ToList();
|
||||
|
||||
foreach (var inboundConnection in otherInboundConnections)
|
||||
if (existingBlockedToken == null)
|
||||
{
|
||||
var blockedToken = Token.Create(inboundConnection.Source.Activity, inboundConnection.Target.Activity, inboundConnection.Source.Port).Block();
|
||||
tokens.Add(blockedToken);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Consume the block.
|
||||
existingBlockedToken.Consume();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Wait for all inbound tokens to be consumed before scheduling the target activity.
|
||||
var inboundConnections = flowGraph.GetForwardInboundConnections(targetActivity);
|
||||
var hasUnconsumed = inboundConnections.Any(inbound =>
|
||||
tokens.Any(t => t is { Consumed: false, Blocked: false } && t.ToActivityId == inbound.Source.Activity.Id)
|
||||
);
|
||||
// Schedule the target.
|
||||
await flowContext.ScheduleActivityAsync(targetActivity, OnChildCompletedTokenBasedLogicAsync);
|
||||
|
||||
if (!hasUnconsumed)
|
||||
{
|
||||
await flowContext.ScheduleActivityAsync(targetActivity, OnChildCompletedTokenBasedLogicAsync);
|
||||
}
|
||||
// Block other inbound connections (adjust per mode if needed).
|
||||
var otherInboundConnections = flowGraph.GetForwardInboundConnections(targetActivity)
|
||||
.Where(x => x.Source.Activity != completedActivity)
|
||||
.ToList();
|
||||
|
||||
foreach (var inboundConnection in otherInboundConnections)
|
||||
{
|
||||
var blockedToken = Token.Create(inboundConnection.Source.Activity, inboundConnection.Target.Activity, inboundConnection.Source.Port).Block();
|
||||
tokens.Add(blockedToken);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Consume the block without scheduling.
|
||||
existingBlockedToken.Consume();
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case MergeMode.Converge:
|
||||
// Strict WaitAll for multiple forwards; schedule on arrival for <=1 (e.g., loops).
|
||||
var inboundConnectionsConverge = flowGraph.GetForwardInboundConnections(targetActivity);
|
||||
|
||||
if (inboundConnectionsConverge.Count > 1)
|
||||
{
|
||||
var hasAllTokens = inboundConnectionsConverge.All(inbound =>
|
||||
tokens.Any(t =>
|
||||
t is { Consumed: false, Blocked: false } &&
|
||||
t.FromActivityId == inbound.Source.Activity.Id &&
|
||||
t.ToActivityId == targetActivity.Id &&
|
||||
t.Outcome == inbound.Source.Port
|
||||
)
|
||||
);
|
||||
|
||||
if (hasAllTokens)
|
||||
await flowContext.ScheduleActivityAsync(targetActivity, OnChildCompletedTokenBasedLogicAsync);
|
||||
}
|
||||
else
|
||||
{
|
||||
await flowContext.ScheduleActivityAsync(targetActivity, OnChildCompletedTokenBasedLogicAsync);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case MergeMode.None:
|
||||
default:
|
||||
// Approximation that proceeds on dead paths.
|
||||
var inboundConnectionsNone = flowGraph.GetForwardInboundConnections(targetActivity);
|
||||
var hasUnconsumed = inboundConnectionsNone.Any(inbound =>
|
||||
tokens.Any(t => !t.Consumed && !t.Blocked && t.ToActivityId == inbound.Source.Activity.Id)
|
||||
);
|
||||
|
||||
if (!hasUnconsumed)
|
||||
await flowContext.ScheduleActivityAsync(targetActivity, OnChildCompletedTokenBasedLogicAsync);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Complete flow if done.
|
||||
var hasPendingWork = flowContext.HasPendingWork();
|
||||
|
||||
if (!hasPendingWork)
|
||||
// Complete flowchart if no pending work.
|
||||
if (!flowContext.HasPendingWork())
|
||||
{
|
||||
tokens.Clear();
|
||||
await flowContext.CompleteActivityAsync();
|
||||
}
|
||||
|
||||
// Purge tokens.
|
||||
// Purge consumed tokens for the completed activity.
|
||||
tokens.RemoveWhere(t => t.ToActivityId == completedActivity.Id && t.Consumed);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,17 +6,24 @@ namespace Elsa.Workflows.Activities.Flowchart.Models;
|
|||
public enum MergeMode
|
||||
{
|
||||
/// <summary>
|
||||
/// Wait for all inbound paths before proceeding.
|
||||
/// No special merging; use approximation that proceeds after all upstream sources complete, ignoring dead paths.
|
||||
/// Suitable for flexible, unstructured merges where optional branches shouldn't block.
|
||||
/// </summary>
|
||||
None,
|
||||
|
||||
/// <summary>
|
||||
/// Strict wait for tokens from all forward inbound connections. Blocks on dead/untaken paths.
|
||||
/// Use for required synchronization points.
|
||||
/// </summary>
|
||||
Converge,
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Proceed when any one inbound path completes; cancel all others.
|
||||
/// Schedule on each arriving token, allowing multiple executions if supported.
|
||||
/// </summary>
|
||||
Race,
|
||||
|
||||
Stream,
|
||||
|
||||
/// <summary>
|
||||
/// Proceed when any one inbound path completes; do not cancel others.
|
||||
/// Schedule on the first arriving token, block or cancel others.
|
||||
/// </summary>
|
||||
Stream
|
||||
Race
|
||||
}
|
||||
|
|
@ -23,5 +23,14 @@
|
|||
<None Update="Scenarios\DependencyWorkflowsPublishing\Workflows\parent.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Scenarios\ImplicitJoins\Workflows\fork-decision-join-converge.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Scenarios\ImplicitJoins\Workflows\fork-decision-join-none.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Scenarios\JoinBehaviors\Workflows\fork-decision-join-waitall.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -47,8 +47,8 @@ public class FlowchartNextActivityTests
|
|||
var writeLineDecision = new FlowSwitch()
|
||||
{
|
||||
Cases = {
|
||||
new FlowSwitchCase("LessThanThree", new Expression("JavaScript", "getVariable('LoopCount') < 3")),
|
||||
new FlowSwitchCase("LessThanOne", new Expression("JavaScript", "getVariable('LoopCount') < 1")),
|
||||
new("LessThanThree", new Expression("JavaScript", "getVariable('LoopCount') < 3")),
|
||||
new("LessThanOne", new Expression("JavaScript", "getVariable('LoopCount') < 1")),
|
||||
},
|
||||
Mode = new(SwitchMode.MatchAny)
|
||||
};
|
||||
|
|
@ -58,13 +58,13 @@ public class FlowchartNextActivityTests
|
|||
var incrementLoop = new SetVariable()
|
||||
{
|
||||
Variable = loopVariable,
|
||||
Value = new Models.Input<object?>(new Expression("JavaScript", "getVariable('LoopCount') + 1"))
|
||||
Value = new(new Expression("JavaScript", "getVariable('LoopCount') + 1"))
|
||||
};
|
||||
var loopbackDecision = new FlowSwitch()
|
||||
{
|
||||
Cases = {
|
||||
new FlowSwitchCase("EqualOne", new Expression("JavaScript", "getVariable('LoopCount') == 1")),
|
||||
new FlowSwitchCase("LessThanFour", new Expression("JavaScript", "getVariable('LoopCount') < 4")),
|
||||
new("EqualOne", new Expression("JavaScript", "getVariable('LoopCount') == 1")),
|
||||
new("LessThanFour", new Expression("JavaScript", "getVariable('LoopCount') < 4")),
|
||||
},
|
||||
Mode = new(SwitchMode.MatchFirst)
|
||||
};
|
||||
|
|
@ -99,19 +99,19 @@ public class FlowchartNextActivityTests
|
|||
{
|
||||
new(start, writeLineDecision),
|
||||
new(dangling, writeLineDecision),
|
||||
new(new Endpoint(writeLineDecision, "LessThanThree"), new Endpoint(a)),
|
||||
new(new Endpoint(writeLineDecision, "LessThanThree"), new Endpoint(b)),
|
||||
new(new Endpoint(writeLineDecision, "LessThanOne"), new Endpoint(c)),
|
||||
new(new Endpoint(writeLineDecision, "Default"), new Endpoint(incrementLoop)),
|
||||
new(new(writeLineDecision, "LessThanThree"), new Endpoint(a)),
|
||||
new(new(writeLineDecision, "LessThanThree"), new Endpoint(b)),
|
||||
new(new(writeLineDecision, "LessThanOne"), new Endpoint(c)),
|
||||
new(new(writeLineDecision, "Default"), new Endpoint(incrementLoop)),
|
||||
new(a, incrementLoop),
|
||||
new(b, incrementLoop),
|
||||
new(c, incrementLoop),
|
||||
new(incrementLoop, loopbackDecision),
|
||||
new(new Endpoint(loopbackDecision, "EqualOne"), new Endpoint(d)),
|
||||
new(new(loopbackDecision, "EqualOne"), new Endpoint(d)),
|
||||
new(d, incrementLoop),
|
||||
new(new Endpoint(loopbackDecision, "LessThanFour"), new Endpoint(e)),
|
||||
new(new(loopbackDecision, "LessThanFour"), new Endpoint(e)),
|
||||
new(e, writeLineDecision),
|
||||
new(new Endpoint(loopbackDecision, "Default"), new Endpoint(f)),
|
||||
new(new(loopbackDecision, "Default"), new Endpoint(f)),
|
||||
new(f, end),
|
||||
}
|
||||
};
|
||||
|
|
@ -193,18 +193,17 @@ public class FlowchartNextActivityTests
|
|||
var incrementLoop = new SetVariable()
|
||||
{
|
||||
Variable = loopVariable,
|
||||
Value = new Models.Input<object?>(new Expression("JavaScript", "getVariable('LoopCount') + 1"))
|
||||
Value = new(new Expression("JavaScript", "getVariable('LoopCount') + 1"))
|
||||
};
|
||||
var loopbackDecision = new FlowSwitch()
|
||||
{
|
||||
Cases = {
|
||||
new FlowSwitchCase("LessThanThree", new Expression("JavaScript", "getVariable('LoopCount') < 3")),
|
||||
new("LessThanThree", new Expression("JavaScript", "getVariable('LoopCount') < 3")),
|
||||
},
|
||||
Mode = new(SwitchMode.MatchFirst)
|
||||
};
|
||||
var end = new End();
|
||||
|
||||
|
||||
|
||||
workflowBuilder.Root = new Flowchart
|
||||
{
|
||||
Variables =
|
||||
|
|
@ -234,8 +233,8 @@ public class FlowchartNextActivityTests
|
|||
new(d, join),
|
||||
new(join, incrementLoop),
|
||||
new(incrementLoop,loopbackDecision),
|
||||
new(new Endpoint(loopbackDecision, "LessThanThree"), new Endpoint(a)),
|
||||
new(new Endpoint(loopbackDecision, "Default"), new Endpoint(end)),
|
||||
new(new(loopbackDecision, "LessThanThree"), new Endpoint(a)),
|
||||
new(new(loopbackDecision, "Default"), new Endpoint(end)),
|
||||
}
|
||||
};
|
||||
});
|
||||
|
|
@ -245,7 +244,6 @@ public class FlowchartNextActivityTests
|
|||
var lines = _capturingTextWriter.Lines.ToList();
|
||||
Assert.Equal(WorkflowSubStatus.Finished, result.WorkflowState.SubStatus);
|
||||
Assert.Equal(new[] { "A", "B", "C", "D", "A", "B", "C", "D", "A", "B", "C", "D"}, lines);
|
||||
|
||||
}
|
||||
|
||||
[Theory(DisplayName = "Flowchart with a Join activity executed multiple times, bug 6479")]
|
||||
|
|
@ -261,7 +259,7 @@ public class FlowchartNextActivityTests
|
|||
var loopbackSwitch = new FlowSwitch()
|
||||
{
|
||||
Cases = {
|
||||
new FlowSwitchCase("DoLoopback", new Expression("JavaScript", "getVariable('LoopCount') < 3")),
|
||||
new("DoLoopback", new Expression("JavaScript", "getVariable('LoopCount') < 3")),
|
||||
},
|
||||
Mode = new(SwitchMode.MatchFirst)
|
||||
};
|
||||
|
|
@ -269,7 +267,7 @@ public class FlowchartNextActivityTests
|
|||
var incrementLoop = new SetVariable()
|
||||
{
|
||||
Variable = loopVariable,
|
||||
Value = new Models.Input<object?>(new Expression("JavaScript", "getVariable('LoopCount') + 1"))
|
||||
Value = new(new Expression("JavaScript", "getVariable('LoopCount') + 1"))
|
||||
};
|
||||
var join = new FlowJoin()
|
||||
{
|
||||
|
|
@ -277,8 +275,7 @@ public class FlowchartNextActivityTests
|
|||
};
|
||||
var b = new WriteLine("B");
|
||||
var end = new End();
|
||||
|
||||
|
||||
|
||||
workflowBuilder.Root = new Flowchart
|
||||
{
|
||||
Variables =
|
||||
|
|
@ -298,9 +295,9 @@ public class FlowchartNextActivityTests
|
|||
Connections =
|
||||
{
|
||||
new(start, loopbackSwitch),
|
||||
new(new Endpoint(loopbackSwitch, "DoLoopback"), new Endpoint(a)),
|
||||
new(new Endpoint(loopbackSwitch, "DoLoopback"), new Endpoint(incrementLoop)),
|
||||
new(new Endpoint(loopbackSwitch, "Default"), new Endpoint(b)),
|
||||
new(new(loopbackSwitch, "DoLoopback"), new Endpoint(a)),
|
||||
new(new(loopbackSwitch, "DoLoopback"), new Endpoint(incrementLoop)),
|
||||
new(new(loopbackSwitch, "Default"), new Endpoint(b)),
|
||||
new(a, join),
|
||||
new(incrementLoop, join),
|
||||
new(join, loopbackSwitch),
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
using Elsa.Testing.Shared;
|
||||
using Elsa.Workflows.IntegrationTests.Scenarios.ImplicitJoins.Workflows;
|
||||
using Elsa.Workflows.IntegrationTests.Scenarios.JoinBehaviors.Workflows;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Elsa.Workflows.IntegrationTests.Scenarios.ImplicitJoins;
|
||||
namespace Elsa.Workflows.IntegrationTests.Scenarios.JoinBehaviors;
|
||||
|
||||
public class BraidedWorkflowTests
|
||||
{
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
using Elsa.Testing.Shared;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Elsa.Workflows.IntegrationTests.Scenarios.JoinBehaviors;
|
||||
|
||||
public class ForkDecisionJoinTests
|
||||
{
|
||||
private readonly CapturingTextWriter _capturingTextWriter = new();
|
||||
private readonly IServiceProvider _services;
|
||||
|
||||
public ForkDecisionJoinTests(ITestOutputHelper testOutputHelper)
|
||||
{
|
||||
_services = new TestApplicationBuilder(testOutputHelper).WithCapturingTextWriter(_capturingTextWriter).Build();
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "The implicit join configured with None merge mode should execute.")]
|
||||
public async Task ImplicitJoinNoneShouldExecute()
|
||||
{
|
||||
await RunAndAssert("fork-decision-join-none.json", ["A", "C"]);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "The implicit join configured with Converge merge mode should not execute.")]
|
||||
public async Task ImplicitJoinConvergeShouldNotExecute()
|
||||
{
|
||||
await RunAndAssert("fork-decision-join-converge.json", ["A"]);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "The explicit join configured with WaitAll join mode should block.")]
|
||||
public async Task ExplicitJoinWaitAllShouldBlock()
|
||||
{
|
||||
await RunAndAssert("fork-decision-join-waitall.json", ["A", "C", "B", "D"]);
|
||||
}
|
||||
|
||||
private async Task RunAndAssert(string workflowFileName, string[] expectedLines)
|
||||
{
|
||||
// Populate registries.
|
||||
await _services.PopulateRegistriesAsync();
|
||||
|
||||
// Import workflow.
|
||||
var workflowDefinition = await _services.ImportWorkflowDefinitionAsync($"Scenarios/JoinBehaviors/Workflows/{workflowFileName}");
|
||||
|
||||
// Execute.
|
||||
await _services.RunWorkflowUntilEndAsync(workflowDefinition.DefinitionId);
|
||||
|
||||
// Assert.
|
||||
var lines = _capturingTextWriter.Lines.ToList();
|
||||
Assert.Equal(expectedLines, lines);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
using Elsa.Testing.Shared;
|
||||
using Elsa.Workflows.IntegrationTests.Scenarios.ImplicitJoins.Workflows;
|
||||
using Elsa.Workflows.IntegrationTests.Scenarios.JoinBehaviors.Workflows;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Elsa.Workflows.IntegrationTests.Scenarios.ImplicitJoins;
|
||||
namespace Elsa.Workflows.IntegrationTests.Scenarios.JoinBehaviors;
|
||||
|
||||
public class ImplicitWorkflowTests
|
||||
{
|
||||
|
|
@ -5,7 +5,7 @@ using Elsa.Workflows.Runtime.Filters;
|
|||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Elsa.Workflows.IntegrationTests.Scenarios.ImplicitJoins;
|
||||
namespace Elsa.Workflows.IntegrationTests.Scenarios.JoinBehaviors;
|
||||
|
||||
public class JoinRunsOnceTests
|
||||
{
|
||||
|
|
@ -24,13 +24,13 @@ public class JoinRunsOnceTests
|
|||
await _services.PopulateRegistriesAsync();
|
||||
|
||||
// Import workflow.
|
||||
var workflowDefinition = await _services.ImportWorkflowDefinitionAsync($"Scenarios/ImplicitJoins/Workflows/join.json");
|
||||
var workflowDefinition = await _services.ImportWorkflowDefinitionAsync($"Scenarios/JoinBehaviors/Workflows/join.json");
|
||||
|
||||
// Execute.
|
||||
var state = await _services.RunWorkflowUntilEndAsync(workflowDefinition.DefinitionId);
|
||||
|
||||
// Assert.
|
||||
var journal = await _services.GetRequiredService<IWorkflowExecutionLogStore>().FindManyAsync(new WorkflowExecutionLogRecordFilter
|
||||
var journal = await _services.GetRequiredService<IWorkflowExecutionLogStore>().FindManyAsync(new()
|
||||
{
|
||||
WorkflowInstanceId = state.Id,
|
||||
ActivityId = "802725996be1b582",
|
||||
|
|
@ -1,11 +1,10 @@
|
|||
using Elsa.Common.Models;
|
||||
using Elsa.Testing.Shared;
|
||||
using Elsa.Workflows.Runtime;
|
||||
using Elsa.Workflows.Runtime.Filters;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Elsa.Workflows.IntegrationTests.Scenarios.ImplicitJoins;
|
||||
namespace Elsa.Workflows.IntegrationTests.Scenarios.JoinBehaviors;
|
||||
|
||||
public class ParallelJoinCompletesTests
|
||||
{
|
||||
|
|
@ -24,7 +23,7 @@ public class ParallelJoinCompletesTests
|
|||
await _services.PopulateRegistriesAsync();
|
||||
|
||||
// Import workflow.
|
||||
var workflowDefinition = await _services.ImportWorkflowDefinitionAsync("Scenarios/ImplicitJoins/Workflows/parallel-join.json");
|
||||
var workflowDefinition = await _services.ImportWorkflowDefinitionAsync("Scenarios/JoinBehaviors/Workflows/parallel-join.json");
|
||||
|
||||
// Execute.
|
||||
var state = await _services.RunWorkflowUntilEndAsync(workflowDefinition.DefinitionId);
|
||||
|
|
@ -2,7 +2,7 @@ using Elsa.Workflows.Activities;
|
|||
using Elsa.Workflows.Activities.Flowchart.Activities;
|
||||
using Elsa.Workflows.Activities.Flowchart.Models;
|
||||
|
||||
namespace Elsa.Workflows.IntegrationTests.Scenarios.ImplicitJoins.Workflows;
|
||||
namespace Elsa.Workflows.IntegrationTests.Scenarios.JoinBehaviors.Workflows;
|
||||
|
||||
public class BraidedWorkflow : WorkflowBase
|
||||
{
|
||||
|
|
@ -33,18 +33,18 @@ public class BraidedWorkflow : WorkflowBase
|
|||
|
||||
Connections =
|
||||
{
|
||||
new Connection(writeLine1, writeLine2),
|
||||
new Connection(writeLine1, writeLine3),
|
||||
new(writeLine1, writeLine2),
|
||||
new(writeLine1, writeLine3),
|
||||
|
||||
new Connection(writeLine2, writeLine4),
|
||||
new Connection(writeLine2, writeLine5),
|
||||
new(writeLine2, writeLine4),
|
||||
new(writeLine2, writeLine5),
|
||||
|
||||
new Connection(writeLine3, writeLine5),
|
||||
new Connection(writeLine3, writeLine6),
|
||||
new(writeLine3, writeLine5),
|
||||
new(writeLine3, writeLine6),
|
||||
|
||||
new Connection(writeLine4, writeLine7),
|
||||
new Connection(writeLine5, writeLine7),
|
||||
new Connection(writeLine6, writeLine7),
|
||||
new(writeLine4, writeLine7),
|
||||
new(writeLine5, writeLine7),
|
||||
new(writeLine6, writeLine7),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@ using Elsa.Workflows.Activities;
|
|||
using Elsa.Workflows.Activities.Flowchart.Activities;
|
||||
using Elsa.Workflows.Activities.Flowchart.Models;
|
||||
|
||||
namespace Elsa.Workflows.IntegrationTests.Scenarios.ImplicitJoins.Workflows;
|
||||
namespace Elsa.Workflows.IntegrationTests.Scenarios.JoinBehaviors.Workflows;
|
||||
|
||||
public class ImplicitLoopWorkflow : WorkflowBase
|
||||
{
|
||||
|
|
@ -30,11 +30,11 @@ public class ImplicitLoopWorkflow : WorkflowBase
|
|||
|
||||
Connections =
|
||||
{
|
||||
new Connection(start, incrementCounter),
|
||||
new Connection(incrementCounter, counterGreaterThanOne),
|
||||
new Connection(new Endpoint(counterGreaterThanOne, "False"), new Endpoint(retry)),
|
||||
new Connection(new Endpoint(counterGreaterThanOne, "True"), new Endpoint(end)),
|
||||
new Connection(retry, incrementCounter),
|
||||
new(start, incrementCounter),
|
||||
new(incrementCounter, counterGreaterThanOne),
|
||||
new(new(counterGreaterThanOne, "False"), new Endpoint(retry)),
|
||||
new(new(counterGreaterThanOne, "True"), new Endpoint(end)),
|
||||
new(retry, incrementCounter),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,253 @@
|
|||
{
|
||||
"$schema": "https://elsaworkflows.io/schemas/workflow-definition/v3.0.0/schema.json",
|
||||
"id": "b60374027329240a",
|
||||
"definitionId": "3d3412c458178fff",
|
||||
"name": "Fork-issues",
|
||||
"description": "show case",
|
||||
"createdAt": "2025-09-30T16:56:12.420213\u002B00:00",
|
||||
"version": 11,
|
||||
"toolVersion": "3.6.0.0",
|
||||
"variables": [
|
||||
{
|
||||
"id": "93b1a1cb9221a635",
|
||||
"name": "myNumber",
|
||||
"typeName": "Int32",
|
||||
"isArray": false,
|
||||
"value": "0",
|
||||
"storageDriverTypeName": "Elsa.Workflows.WorkflowInstanceStorageDriver, Elsa.Workflows.Core"
|
||||
}
|
||||
],
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"outcomes": [],
|
||||
"customProperties": {
|
||||
"VariableTestValues": {}
|
||||
},
|
||||
"isReadonly": false,
|
||||
"isSystem": false,
|
||||
"isLatest": true,
|
||||
"isPublished": true,
|
||||
"options": {
|
||||
"autoUpdateConsumingWorkflows": false
|
||||
},
|
||||
"root": {
|
||||
"id": "8d3ecf38f4425d86",
|
||||
"nodeId": "Workflow1:8d3ecf38f4425d86",
|
||||
"name": "Flowchart1",
|
||||
"type": "Elsa.Flowchart",
|
||||
"version": 1,
|
||||
"customProperties": {
|
||||
"notFoundConnections": [],
|
||||
"canStartWorkflow": false,
|
||||
"runAsynchronously": false
|
||||
},
|
||||
"metadata": {},
|
||||
"activities": [
|
||||
{
|
||||
"condition": {
|
||||
"typeName": "Boolean",
|
||||
"expression": {
|
||||
"type": "JavaScript",
|
||||
"value": "return false;"
|
||||
}
|
||||
},
|
||||
"id": "35c0f40b240666c6",
|
||||
"nodeId": "Workflow1:8d3ecf38f4425d86:35c0f40b240666c6",
|
||||
"name": "FlowDecision1",
|
||||
"type": "Elsa.FlowDecision",
|
||||
"version": 1,
|
||||
"customProperties": {
|
||||
"canStartWorkflow": false,
|
||||
"runAsynchronously": false
|
||||
},
|
||||
"metadata": {
|
||||
"designer": {
|
||||
"position": {
|
||||
"x": 337.5703125,
|
||||
"y": 161.96484375
|
||||
},
|
||||
"size": {
|
||||
"width": 149.84375,
|
||||
"height": 67.9765625
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "342cf539286a991d",
|
||||
"nodeId": "Workflow1:8d3ecf38f4425d86:342cf539286a991d",
|
||||
"name": "Start1",
|
||||
"type": "Elsa.Start",
|
||||
"version": 1,
|
||||
"customProperties": {
|
||||
"canStartWorkflow": false,
|
||||
"runAsynchronously": false
|
||||
},
|
||||
"metadata": {
|
||||
"designer": {
|
||||
"position": {
|
||||
"x": 96.32421875,
|
||||
"y": 48.98828125
|
||||
},
|
||||
"size": {
|
||||
"width": 122.6484375,
|
||||
"height": 67.9765625
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"text": {
|
||||
"typeName": "String",
|
||||
"expression": {
|
||||
"type": "Literal",
|
||||
"value": "A"
|
||||
}
|
||||
},
|
||||
"id": "79e963ee9889c30a",
|
||||
"nodeId": "Workflow1:8d3ecf38f4425d86:79e963ee9889c30a",
|
||||
"name": "WriteLine1",
|
||||
"type": "Elsa.WriteLine",
|
||||
"version": 1,
|
||||
"customProperties": {
|
||||
"canStartWorkflow": false,
|
||||
"runAsynchronously": false
|
||||
},
|
||||
"metadata": {
|
||||
"designer": {
|
||||
"position": {
|
||||
"x": 609.34375,
|
||||
"y": 48.98828125
|
||||
},
|
||||
"size": {
|
||||
"width": 183.703125,
|
||||
"height": 67.9765625
|
||||
}
|
||||
},
|
||||
"displayText": "A"
|
||||
}
|
||||
},
|
||||
{
|
||||
"text": {
|
||||
"typeName": "String",
|
||||
"expression": {
|
||||
"type": "Literal",
|
||||
"value": "B"
|
||||
}
|
||||
},
|
||||
"id": "af2cc31d1cef7a90",
|
||||
"nodeId": "Workflow1:8d3ecf38f4425d86:af2cc31d1cef7a90",
|
||||
"name": "WriteLine9",
|
||||
"type": "Elsa.WriteLine",
|
||||
"version": 1,
|
||||
"customProperties": {
|
||||
"canStartWorkflow": false,
|
||||
"runAsynchronously": false
|
||||
},
|
||||
"metadata": {
|
||||
"designer": {
|
||||
"position": {
|
||||
"x": 609.34375,
|
||||
"y": 161.96484375
|
||||
},
|
||||
"size": {
|
||||
"width": 159.6171875,
|
||||
"height": 67.9765625
|
||||
}
|
||||
},
|
||||
"displayText": "B"
|
||||
}
|
||||
},
|
||||
{
|
||||
"text": {
|
||||
"typeName": "String",
|
||||
"expression": {
|
||||
"type": "Literal",
|
||||
"value": "C"
|
||||
}
|
||||
},
|
||||
"id": "768b5a7e58c3ecf8",
|
||||
"nodeId": "Workflow1:8d3ecf38f4425d86:768b5a7e58c3ecf8",
|
||||
"name": "WriteLine4",
|
||||
"type": "Elsa.WriteLine",
|
||||
"version": 1,
|
||||
"customProperties": {
|
||||
"canStartWorkflow": false,
|
||||
"runAsynchronously": false,
|
||||
"mergeMode": "Converge"
|
||||
},
|
||||
"metadata": {
|
||||
"designer": {
|
||||
"position": {
|
||||
"x": 885.923828125,
|
||||
"y": 48.98828125
|
||||
},
|
||||
"size": {
|
||||
"width": 159.6171875,
|
||||
"height": 67.9765625
|
||||
}
|
||||
},
|
||||
"displayText": "C"
|
||||
}
|
||||
}
|
||||
],
|
||||
"variables": [],
|
||||
"connections": [
|
||||
{
|
||||
"source": {
|
||||
"activity": "79e963ee9889c30a",
|
||||
"port": "Done"
|
||||
},
|
||||
"target": {
|
||||
"activity": "768b5a7e58c3ecf8",
|
||||
"port": "In"
|
||||
},
|
||||
"vertices": []
|
||||
},
|
||||
{
|
||||
"source": {
|
||||
"activity": "af2cc31d1cef7a90",
|
||||
"port": "Done"
|
||||
},
|
||||
"target": {
|
||||
"activity": "768b5a7e58c3ecf8",
|
||||
"port": "In"
|
||||
},
|
||||
"vertices": []
|
||||
},
|
||||
{
|
||||
"source": {
|
||||
"activity": "342cf539286a991d",
|
||||
"port": "Done"
|
||||
},
|
||||
"target": {
|
||||
"activity": "79e963ee9889c30a",
|
||||
"port": "In"
|
||||
},
|
||||
"vertices": []
|
||||
},
|
||||
{
|
||||
"source": {
|
||||
"activity": "342cf539286a991d",
|
||||
"port": "Done"
|
||||
},
|
||||
"target": {
|
||||
"activity": "35c0f40b240666c6",
|
||||
"port": "In"
|
||||
},
|
||||
"vertices": []
|
||||
},
|
||||
{
|
||||
"source": {
|
||||
"activity": "35c0f40b240666c6",
|
||||
"port": "True"
|
||||
},
|
||||
"target": {
|
||||
"activity": "af2cc31d1cef7a90",
|
||||
"port": "In"
|
||||
},
|
||||
"vertices": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,253 @@
|
|||
{
|
||||
"$schema": "https://elsaworkflows.io/schemas/workflow-definition/v3.0.0/schema.json",
|
||||
"id": "eed88d95ad53e0f",
|
||||
"definitionId": "3d3412c458178fff",
|
||||
"name": "Fork-issues",
|
||||
"description": "show case",
|
||||
"createdAt": "2025-09-30T17:03:24.623714\u002B00:00",
|
||||
"version": 12,
|
||||
"toolVersion": "3.6.0.0",
|
||||
"variables": [
|
||||
{
|
||||
"id": "93b1a1cb9221a635",
|
||||
"name": "myNumber",
|
||||
"typeName": "Int32",
|
||||
"isArray": false,
|
||||
"value": "0",
|
||||
"storageDriverTypeName": "Elsa.Workflows.WorkflowInstanceStorageDriver, Elsa.Workflows.Core"
|
||||
}
|
||||
],
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"outcomes": [],
|
||||
"customProperties": {
|
||||
"VariableTestValues": {}
|
||||
},
|
||||
"isReadonly": false,
|
||||
"isSystem": false,
|
||||
"isLatest": true,
|
||||
"isPublished": true,
|
||||
"options": {
|
||||
"autoUpdateConsumingWorkflows": false
|
||||
},
|
||||
"root": {
|
||||
"id": "8d3ecf38f4425d86",
|
||||
"nodeId": "Workflow1:8d3ecf38f4425d86",
|
||||
"name": "Flowchart1",
|
||||
"type": "Elsa.Flowchart",
|
||||
"version": 1,
|
||||
"customProperties": {
|
||||
"notFoundConnections": [],
|
||||
"canStartWorkflow": false,
|
||||
"runAsynchronously": false
|
||||
},
|
||||
"metadata": {},
|
||||
"activities": [
|
||||
{
|
||||
"condition": {
|
||||
"typeName": "Boolean",
|
||||
"expression": {
|
||||
"type": "JavaScript",
|
||||
"value": "return false;"
|
||||
}
|
||||
},
|
||||
"id": "35c0f40b240666c6",
|
||||
"nodeId": "Workflow1:8d3ecf38f4425d86:35c0f40b240666c6",
|
||||
"name": "FlowDecision1",
|
||||
"type": "Elsa.FlowDecision",
|
||||
"version": 1,
|
||||
"customProperties": {
|
||||
"canStartWorkflow": false,
|
||||
"runAsynchronously": false
|
||||
},
|
||||
"metadata": {
|
||||
"designer": {
|
||||
"position": {
|
||||
"x": 337.5703125,
|
||||
"y": 161.96484375
|
||||
},
|
||||
"size": {
|
||||
"width": 149.84375,
|
||||
"height": 67.9765625
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "342cf539286a991d",
|
||||
"nodeId": "Workflow1:8d3ecf38f4425d86:342cf539286a991d",
|
||||
"name": "Start1",
|
||||
"type": "Elsa.Start",
|
||||
"version": 1,
|
||||
"customProperties": {
|
||||
"canStartWorkflow": false,
|
||||
"runAsynchronously": false
|
||||
},
|
||||
"metadata": {
|
||||
"designer": {
|
||||
"position": {
|
||||
"x": 96.32421875,
|
||||
"y": 48.98828125
|
||||
},
|
||||
"size": {
|
||||
"width": 122.6484375,
|
||||
"height": 67.9765625
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"text": {
|
||||
"typeName": "String",
|
||||
"expression": {
|
||||
"type": "Literal",
|
||||
"value": "A"
|
||||
}
|
||||
},
|
||||
"id": "79e963ee9889c30a",
|
||||
"nodeId": "Workflow1:8d3ecf38f4425d86:79e963ee9889c30a",
|
||||
"name": "WriteLine1",
|
||||
"type": "Elsa.WriteLine",
|
||||
"version": 1,
|
||||
"customProperties": {
|
||||
"canStartWorkflow": false,
|
||||
"runAsynchronously": false
|
||||
},
|
||||
"metadata": {
|
||||
"designer": {
|
||||
"position": {
|
||||
"x": 609.34375,
|
||||
"y": 48.98828125
|
||||
},
|
||||
"size": {
|
||||
"width": 183.703125,
|
||||
"height": 67.9765625
|
||||
}
|
||||
},
|
||||
"displayText": "A"
|
||||
}
|
||||
},
|
||||
{
|
||||
"text": {
|
||||
"typeName": "String",
|
||||
"expression": {
|
||||
"type": "Literal",
|
||||
"value": "B"
|
||||
}
|
||||
},
|
||||
"id": "af2cc31d1cef7a90",
|
||||
"nodeId": "Workflow1:8d3ecf38f4425d86:af2cc31d1cef7a90",
|
||||
"name": "WriteLine9",
|
||||
"type": "Elsa.WriteLine",
|
||||
"version": 1,
|
||||
"customProperties": {
|
||||
"canStartWorkflow": false,
|
||||
"runAsynchronously": false
|
||||
},
|
||||
"metadata": {
|
||||
"designer": {
|
||||
"position": {
|
||||
"x": 609.34375,
|
||||
"y": 161.96484375
|
||||
},
|
||||
"size": {
|
||||
"width": 159.6171875,
|
||||
"height": 67.9765625
|
||||
}
|
||||
},
|
||||
"displayText": "B"
|
||||
}
|
||||
},
|
||||
{
|
||||
"text": {
|
||||
"typeName": "String",
|
||||
"expression": {
|
||||
"type": "Literal",
|
||||
"value": "C"
|
||||
}
|
||||
},
|
||||
"id": "768b5a7e58c3ecf8",
|
||||
"nodeId": "Workflow1:8d3ecf38f4425d86:768b5a7e58c3ecf8",
|
||||
"name": "WriteLine4",
|
||||
"type": "Elsa.WriteLine",
|
||||
"version": 1,
|
||||
"customProperties": {
|
||||
"canStartWorkflow": false,
|
||||
"runAsynchronously": false,
|
||||
"mergeMode": "None"
|
||||
},
|
||||
"metadata": {
|
||||
"designer": {
|
||||
"position": {
|
||||
"x": 885.923828125,
|
||||
"y": 48.98828125
|
||||
},
|
||||
"size": {
|
||||
"width": 159.6171875,
|
||||
"height": 67.9765625
|
||||
}
|
||||
},
|
||||
"displayText": "C"
|
||||
}
|
||||
}
|
||||
],
|
||||
"variables": [],
|
||||
"connections": [
|
||||
{
|
||||
"source": {
|
||||
"activity": "79e963ee9889c30a",
|
||||
"port": "Done"
|
||||
},
|
||||
"target": {
|
||||
"activity": "768b5a7e58c3ecf8",
|
||||
"port": "In"
|
||||
},
|
||||
"vertices": []
|
||||
},
|
||||
{
|
||||
"source": {
|
||||
"activity": "af2cc31d1cef7a90",
|
||||
"port": "Done"
|
||||
},
|
||||
"target": {
|
||||
"activity": "768b5a7e58c3ecf8",
|
||||
"port": "In"
|
||||
},
|
||||
"vertices": []
|
||||
},
|
||||
{
|
||||
"source": {
|
||||
"activity": "342cf539286a991d",
|
||||
"port": "Done"
|
||||
},
|
||||
"target": {
|
||||
"activity": "79e963ee9889c30a",
|
||||
"port": "In"
|
||||
},
|
||||
"vertices": []
|
||||
},
|
||||
{
|
||||
"source": {
|
||||
"activity": "342cf539286a991d",
|
||||
"port": "Done"
|
||||
},
|
||||
"target": {
|
||||
"activity": "35c0f40b240666c6",
|
||||
"port": "In"
|
||||
},
|
||||
"vertices": []
|
||||
},
|
||||
{
|
||||
"source": {
|
||||
"activity": "35c0f40b240666c6",
|
||||
"port": "True"
|
||||
},
|
||||
"target": {
|
||||
"activity": "af2cc31d1cef7a90",
|
||||
"port": "In"
|
||||
},
|
||||
"vertices": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,451 @@
|
|||
{
|
||||
"$schema": "https://elsaworkflows.io/schemas/workflow-definition/v3.0.0/schema.json",
|
||||
"id": "93277d4cd53f2dfc",
|
||||
"definitionId": "3d3412c458178fff",
|
||||
"name": "Fork-issues",
|
||||
"description": "show case",
|
||||
"createdAt": "2025-09-30T17:19:02.380015\u002B00:00",
|
||||
"version": 14,
|
||||
"toolVersion": "3.6.0.0",
|
||||
"variables": [
|
||||
{
|
||||
"id": "93b1a1cb9221a635",
|
||||
"name": "myNumber",
|
||||
"typeName": "Int32",
|
||||
"isArray": false,
|
||||
"value": "0"
|
||||
}
|
||||
],
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"outcomes": [],
|
||||
"customProperties": {
|
||||
"VariableTestValues": {}
|
||||
},
|
||||
"isReadonly": false,
|
||||
"isSystem": false,
|
||||
"isLatest": true,
|
||||
"isPublished": true,
|
||||
"options": {
|
||||
"autoUpdateConsumingWorkflows": false
|
||||
},
|
||||
"root": {
|
||||
"id": "8d3ecf38f4425d86",
|
||||
"nodeId": "Workflow1:8d3ecf38f4425d86",
|
||||
"type": "Elsa.Flowchart",
|
||||
"version": 1,
|
||||
"customProperties": {
|
||||
"source": "FlowchartJsonConverter.cs:45",
|
||||
"notFoundConnections": [],
|
||||
"canStartWorkflow": false,
|
||||
"runAsynchronously": false
|
||||
},
|
||||
"metadata": {},
|
||||
"activities": [
|
||||
{
|
||||
"mode": {
|
||||
"typeName": "Elsa.Workflows.Activities.Flowchart.Models.FlowJoinMode, Elsa.Workflows.Core",
|
||||
"expression": {
|
||||
"type": "Literal",
|
||||
"value": "WaitAll"
|
||||
}
|
||||
},
|
||||
"id": "80847eb320f89c72",
|
||||
"nodeId": "Workflow1:8d3ecf38f4425d86:80847eb320f89c72",
|
||||
"name": "FlowJoin1",
|
||||
"type": "Elsa.FlowJoin",
|
||||
"version": 1,
|
||||
"customProperties": {
|
||||
"canStartWorkflow": false,
|
||||
"runAsynchronously": false
|
||||
},
|
||||
"metadata": {
|
||||
"designer": {
|
||||
"position": {
|
||||
"x": 1241.6124954223633,
|
||||
"y": -1.100006103515625
|
||||
},
|
||||
"size": {
|
||||
"width": 118.7734375,
|
||||
"height": 67.9765625
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"text": {
|
||||
"typeName": "String",
|
||||
"expression": {
|
||||
"type": "Literal",
|
||||
"value": "F"
|
||||
}
|
||||
},
|
||||
"id": "8558cd1571aff0e8",
|
||||
"nodeId": "Workflow1:8d3ecf38f4425d86:8558cd1571aff0e8",
|
||||
"name": "F",
|
||||
"type": "Elsa.WriteLine",
|
||||
"version": 1,
|
||||
"customProperties": {
|
||||
"canStartWorkflow": false,
|
||||
"runAsynchronously": false
|
||||
},
|
||||
"metadata": {
|
||||
"designer": {
|
||||
"position": {
|
||||
"x": 1511.3999633789062,
|
||||
"y": -1.100006103515625
|
||||
},
|
||||
"size": {
|
||||
"width": 197.703125,
|
||||
"height": 67.9765625
|
||||
}
|
||||
},
|
||||
"displayText": "F"
|
||||
}
|
||||
},
|
||||
{
|
||||
"text": {
|
||||
"typeName": "String",
|
||||
"expression": {
|
||||
"type": "Literal",
|
||||
"value": "E"
|
||||
}
|
||||
},
|
||||
"id": "af2cc31d1cef7a90",
|
||||
"nodeId": "Workflow1:8d3ecf38f4425d86:af2cc31d1cef7a90",
|
||||
"name": "E",
|
||||
"type": "Elsa.WriteLine",
|
||||
"version": 1,
|
||||
"customProperties": {
|
||||
"canStartWorkflow": false,
|
||||
"runAsynchronously": false
|
||||
},
|
||||
"metadata": {
|
||||
"designer": {
|
||||
"position": {
|
||||
"x": 900,
|
||||
"y": 95.92343139648438
|
||||
},
|
||||
"size": {
|
||||
"width": 159.6171875,
|
||||
"height": 67.9765625
|
||||
}
|
||||
},
|
||||
"displayText": "E"
|
||||
}
|
||||
},
|
||||
{
|
||||
"condition": {
|
||||
"typeName": "Boolean",
|
||||
"expression": {
|
||||
"type": "JavaScript",
|
||||
"value": "return false;"
|
||||
}
|
||||
},
|
||||
"id": "35c0f40b240666c6",
|
||||
"nodeId": "Workflow1:8d3ecf38f4425d86:35c0f40b240666c6",
|
||||
"name": "FlowDecision1",
|
||||
"type": "Elsa.FlowDecision",
|
||||
"version": 1,
|
||||
"customProperties": {
|
||||
"canStartWorkflow": false,
|
||||
"runAsynchronously": false
|
||||
},
|
||||
"metadata": {
|
||||
"designer": {
|
||||
"position": {
|
||||
"x": 649.8249664306641,
|
||||
"y": 163.89999389648438
|
||||
},
|
||||
"size": {
|
||||
"width": 149.84375,
|
||||
"height": 67.9765625
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"code": {
|
||||
"typeName": "String",
|
||||
"expression": {
|
||||
"type": "Literal",
|
||||
"value": "110002"
|
||||
}
|
||||
},
|
||||
"category": {
|
||||
"typeName": "String",
|
||||
"expression": {
|
||||
"type": "Literal",
|
||||
"value": "failure"
|
||||
}
|
||||
},
|
||||
"faultType": null,
|
||||
"message": {
|
||||
"typeName": "String",
|
||||
"expression": {
|
||||
"type": "Literal",
|
||||
"value": "that went south"
|
||||
}
|
||||
},
|
||||
"id": "cfffbc06a0b6eeb0",
|
||||
"nodeId": "Workflow1:8d3ecf38f4425d86:cfffbc06a0b6eeb0",
|
||||
"name": "Fault1",
|
||||
"type": "Elsa.Fault",
|
||||
"version": 1,
|
||||
"customProperties": {
|
||||
"canStartWorkflow": false,
|
||||
"runAsynchronously": false
|
||||
},
|
||||
"metadata": {
|
||||
"designer": {
|
||||
"position": {
|
||||
"x": 900,
|
||||
"y": 254.2125015258789
|
||||
},
|
||||
"size": {
|
||||
"width": 123.453125,
|
||||
"height": 67.9765625
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"text": {
|
||||
"typeName": "String",
|
||||
"expression": {
|
||||
"type": "Literal",
|
||||
"value": "D"
|
||||
}
|
||||
},
|
||||
"id": "896ddf80e59c66ef",
|
||||
"nodeId": "Workflow1:8d3ecf38f4425d86:896ddf80e59c66ef",
|
||||
"name": "D",
|
||||
"type": "Elsa.WriteLine",
|
||||
"version": 1,
|
||||
"customProperties": {
|
||||
"canStartWorkflow": false,
|
||||
"runAsynchronously": false
|
||||
},
|
||||
"metadata": {
|
||||
"designer": {
|
||||
"position": {
|
||||
"x": 327.39996337890625,
|
||||
"y": 163.89999389648438
|
||||
},
|
||||
"size": {
|
||||
"width": 242.828125,
|
||||
"height": 67.9765625
|
||||
}
|
||||
},
|
||||
"displayText": "D"
|
||||
}
|
||||
},
|
||||
{
|
||||
"text": {
|
||||
"typeName": "String",
|
||||
"expression": {
|
||||
"type": "Literal",
|
||||
"value": "A"
|
||||
}
|
||||
},
|
||||
"id": "79e963ee9889c30a",
|
||||
"nodeId": "Workflow1:8d3ecf38f4425d86:79e963ee9889c30a",
|
||||
"name": "A",
|
||||
"type": "Elsa.WriteLine",
|
||||
"version": 1,
|
||||
"customProperties": {
|
||||
"canStartWorkflow": false,
|
||||
"runAsynchronously": false
|
||||
},
|
||||
"metadata": {
|
||||
"designer": {
|
||||
"position": {
|
||||
"x": -20,
|
||||
"y": -1.100006103515625
|
||||
},
|
||||
"size": {
|
||||
"width": 183.703125,
|
||||
"height": 67.9765625
|
||||
}
|
||||
},
|
||||
"displayText": "A"
|
||||
}
|
||||
},
|
||||
{
|
||||
"text": {
|
||||
"typeName": "String",
|
||||
"expression": {
|
||||
"type": "Literal",
|
||||
"value": "C"
|
||||
}
|
||||
},
|
||||
"id": "7302df5350ea8095",
|
||||
"nodeId": "Workflow1:8d3ecf38f4425d86:7302df5350ea8095",
|
||||
"name": "C",
|
||||
"type": "Elsa.WriteLine",
|
||||
"version": 1,
|
||||
"customProperties": {
|
||||
"canStartWorkflow": false,
|
||||
"runAsynchronously": false
|
||||
},
|
||||
"metadata": {
|
||||
"designer": {
|
||||
"position": {
|
||||
"x": 327.39996337890625,
|
||||
"y": -1.100006103515625
|
||||
},
|
||||
"size": {
|
||||
"width": 242.828125,
|
||||
"height": 67.9765625
|
||||
}
|
||||
},
|
||||
"displayText": "C"
|
||||
}
|
||||
},
|
||||
{
|
||||
"text": {
|
||||
"typeName": "String",
|
||||
"expression": {
|
||||
"type": "Literal",
|
||||
"value": "B"
|
||||
}
|
||||
},
|
||||
"id": "c1a7d756c9ad7597",
|
||||
"nodeId": "Workflow1:8d3ecf38f4425d86:c1a7d756c9ad7597",
|
||||
"name": "B",
|
||||
"type": "Elsa.WriteLine",
|
||||
"version": 1,
|
||||
"customProperties": {
|
||||
"canStartWorkflow": false,
|
||||
"runAsynchronously": false
|
||||
},
|
||||
"metadata": {
|
||||
"designer": {
|
||||
"position": {
|
||||
"x": 327.39996337890625,
|
||||
"y": -144.10000610351562
|
||||
},
|
||||
"size": {
|
||||
"width": 242.828125,
|
||||
"height": 67.9765625
|
||||
}
|
||||
},
|
||||
"displayText": "B"
|
||||
}
|
||||
}
|
||||
],
|
||||
"variables": [],
|
||||
"connections": [
|
||||
{
|
||||
"source": {
|
||||
"activity": "79e963ee9889c30a",
|
||||
"port": "Done"
|
||||
},
|
||||
"target": {
|
||||
"activity": "7302df5350ea8095",
|
||||
"port": "In"
|
||||
},
|
||||
"vertices": []
|
||||
},
|
||||
{
|
||||
"source": {
|
||||
"activity": "79e963ee9889c30a",
|
||||
"port": "Done"
|
||||
},
|
||||
"target": {
|
||||
"activity": "c1a7d756c9ad7597",
|
||||
"port": "In"
|
||||
},
|
||||
"vertices": []
|
||||
},
|
||||
{
|
||||
"source": {
|
||||
"activity": "79e963ee9889c30a",
|
||||
"port": "Done"
|
||||
},
|
||||
"target": {
|
||||
"activity": "896ddf80e59c66ef",
|
||||
"port": "In"
|
||||
},
|
||||
"vertices": []
|
||||
},
|
||||
{
|
||||
"source": {
|
||||
"activity": "35c0f40b240666c6",
|
||||
"port": "True"
|
||||
},
|
||||
"target": {
|
||||
"activity": "af2cc31d1cef7a90",
|
||||
"port": "In"
|
||||
},
|
||||
"vertices": []
|
||||
},
|
||||
{
|
||||
"source": {
|
||||
"activity": "896ddf80e59c66ef",
|
||||
"port": "Done"
|
||||
},
|
||||
"target": {
|
||||
"activity": "35c0f40b240666c6",
|
||||
"port": "In"
|
||||
},
|
||||
"vertices": []
|
||||
},
|
||||
{
|
||||
"source": {
|
||||
"activity": "35c0f40b240666c6",
|
||||
"port": "False"
|
||||
},
|
||||
"target": {
|
||||
"activity": "cfffbc06a0b6eeb0",
|
||||
"port": "In"
|
||||
},
|
||||
"vertices": []
|
||||
},
|
||||
{
|
||||
"source": {
|
||||
"activity": "c1a7d756c9ad7597",
|
||||
"port": "Done"
|
||||
},
|
||||
"target": {
|
||||
"activity": "80847eb320f89c72",
|
||||
"port": "In"
|
||||
},
|
||||
"vertices": []
|
||||
},
|
||||
{
|
||||
"source": {
|
||||
"activity": "80847eb320f89c72",
|
||||
"port": "Done"
|
||||
},
|
||||
"target": {
|
||||
"activity": "8558cd1571aff0e8",
|
||||
"port": "In"
|
||||
},
|
||||
"vertices": []
|
||||
},
|
||||
{
|
||||
"source": {
|
||||
"activity": "7302df5350ea8095",
|
||||
"port": "Done"
|
||||
},
|
||||
"target": {
|
||||
"activity": "80847eb320f89c72",
|
||||
"port": "In"
|
||||
},
|
||||
"vertices": []
|
||||
},
|
||||
{
|
||||
"source": {
|
||||
"activity": "af2cc31d1cef7a90",
|
||||
"port": "Done"
|
||||
},
|
||||
"target": {
|
||||
"activity": "80847eb320f89c72",
|
||||
"port": "In"
|
||||
},
|
||||
"vertices": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue