Merge branch 'develop/3.6.0' into feat/test-guidelines
This commit is contained in:
commit
4d17f869a3
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
|
||||
}
|
||||
|
|
@ -3,6 +3,9 @@ using Elsa.Extensions;
|
|||
using Elsa.Expressions.JavaScript.TypeDefinitions.Abstractions;
|
||||
using Elsa.Expressions.JavaScript.TypeDefinitions.Contracts;
|
||||
using Elsa.Expressions.JavaScript.TypeDefinitions.Models;
|
||||
using Elsa.Workflows.Management.Options;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Elsa.Expressions.JavaScript.Providers;
|
||||
|
||||
|
|
|
|||
|
|
@ -222,7 +222,7 @@ public class HttpFeature(IModule module) : FeatureBase(module)
|
|||
.AddScoped<IDownloadableContentHandler, HttpFileDownloadableContentHandler>()
|
||||
|
||||
//Trigger payload validators.
|
||||
.AddTriggerPaylodValidator<HttpEndpointTriggerPayloadValidator, HttpEndpointBookmarkPayload>()
|
||||
.AddTriggerPayloadValidator<HttpEndpointTriggerPayloadValidator, HttpEndpointBookmarkPayload>()
|
||||
|
||||
// File caches.
|
||||
.AddScoped(FileCache)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
namespace Elsa.Scheduling.Bookmarks;
|
||||
|
||||
internal record CronBookmarkPayload(DateTimeOffset ExecuteAt, string CronExpression);
|
||||
public record CronBookmarkPayload(DateTimeOffset ExecuteAt, string CronExpression);
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
namespace Elsa.Scheduling.Bookmarks;
|
||||
|
||||
internal record StartAtPayload(DateTimeOffset ExecuteAt);
|
||||
public record StartAtPayload(DateTimeOffset ExecuteAt);
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
namespace Elsa.Scheduling.Bookmarks;
|
||||
|
||||
internal record TimerBookmarkPayload(DateTimeOffset ResumeAt);
|
||||
public record TimerBookmarkPayload(DateTimeOffset ResumeAt);
|
||||
|
|
@ -48,13 +48,12 @@ public class SchedulingFeature : FeatureBase
|
|||
.AddScoped<ITriggerScheduler, DefaultTriggerScheduler>()
|
||||
.AddScoped<IBookmarkScheduler, DefaultBookmarkScheduler>()
|
||||
.AddScoped<DefaultWorkflowScheduler>()
|
||||
.AddSingleton(CronParser)
|
||||
.AddScoped(WorkflowScheduler)
|
||||
.AddBackgroundTask<CreateSchedulesBackgroundTask>()
|
||||
.AddHandlersFrom<ScheduleWorkflows>()
|
||||
|
||||
//Trigger payload validators.
|
||||
.AddTriggerPaylodValidator<CronTriggerPayloadValidator, CronTriggerPayload>();
|
||||
.AddTriggerPayloadValidator<CronTriggerPayloadValidator, CronTriggerPayload>();
|
||||
|
||||
Module.Configure<WorkflowManagementFeature>(management => management.AddActivitiesFrom<SchedulingFeature>());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -86,41 +86,49 @@ public class ScheduledRecurringTask : IScheduledTask, IDisposable
|
|||
|
||||
_timer.Elapsed += async (_, _) =>
|
||||
{
|
||||
_timer?.Dispose();
|
||||
_timer = null;
|
||||
_startAt = _systemClock.UtcNow + _interval;
|
||||
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var commandSender = scope.ServiceProvider.GetRequiredService<ICommandSender>();
|
||||
var cancellationToken = _cancellationTokenSource.Token;
|
||||
if (!cancellationToken.IsCancellationRequested)
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
var acquired = await _executionSemaphore.WaitAsync(0, cancellationToken);
|
||||
if (!acquired) return;
|
||||
_executing = true;
|
||||
await commandSender.SendAsync(new RunScheduledTask(_task), cancellationToken);
|
||||
|
||||
if (_cancellationRequested)
|
||||
_timer?.Dispose();
|
||||
_timer = null;
|
||||
_startAt = _systemClock.UtcNow + _interval;
|
||||
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var commandSender = scope.ServiceProvider.GetRequiredService<ICommandSender>();
|
||||
var cancellationToken = _cancellationTokenSource.Token;
|
||||
if (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
_cancellationRequested = false;
|
||||
_cancellationTokenSource.Cancel();
|
||||
var acquired = await _executionSemaphore.WaitAsync(0, cancellationToken);
|
||||
if (!acquired) return;
|
||||
_executing = true;
|
||||
await commandSender.SendAsync(new RunScheduledTask(_task), cancellationToken);
|
||||
|
||||
if (_cancellationRequested)
|
||||
{
|
||||
_cancellationRequested = false;
|
||||
_cancellationTokenSource.Cancel();
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Error executing scheduled task");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_executing = false;
|
||||
_executionSemaphore.Release();
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Error executing scheduled task");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_executing = false;
|
||||
_executionSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
if (!cancellationToken.IsCancellationRequested)
|
||||
Schedule();
|
||||
if (!cancellationToken.IsCancellationRequested)
|
||||
Schedule();
|
||||
}
|
||||
catch (ObjectDisposedException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Service Provider was disposed.");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -29,19 +29,24 @@ internal class PostEndpoint(
|
|||
{
|
||||
PostRequest? request = null;
|
||||
|
||||
if (HttpContext.Request.ContentLength > 0 && (HttpContext.Request.ContentType?.Contains("application/json") ?? true))
|
||||
if (HttpContext.Request.ContentType?.Contains("application/json") ?? false)
|
||||
{
|
||||
try
|
||||
using var reader = new StreamReader(HttpContext.Request.Body);
|
||||
var body = await reader.ReadToEndAsync();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(body))
|
||||
{
|
||||
request = await JsonSerializer.DeserializeAsync<PostRequest>(HttpContext.Request.Body,
|
||||
new JsonSerializerOptions
|
||||
try
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
}, cancellationToken: cancellationToken);
|
||||
}
|
||||
catch
|
||||
{
|
||||
AddError("Invalid request body.");
|
||||
request = JsonSerializer.Deserialize<PostRequest>(body, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
});
|
||||
}
|
||||
catch
|
||||
{
|
||||
AddError("Invalid request body.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ public partial class Flowchart : Container
|
|||
/// <summary>
|
||||
/// The activity to execute when the flowchart starts.
|
||||
/// </summary>
|
||||
[Port] [Browsable(false)] public IActivity? Start { get; set; }
|
||||
[Port][Browsable(false)] public IActivity? Start { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A list of connections between activities.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -59,6 +59,15 @@ public interface IWorkflowDefinitionPublisher
|
|||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The retracted workflow definition.</returns>
|
||||
Task<WorkflowDefinition> RetractAsync(WorkflowDefinition definition, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new workflow definition from the specified version.
|
||||
/// </summary>
|
||||
/// <param name="definitionId">The definition ID.</param>
|
||||
/// <param name="version">The version number.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The new workflow definition.</returns>
|
||||
Task<WorkflowDefinition> RevertVersionAsync(string definitionId, int version, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or creates a draft for the specified workflow definition.
|
||||
|
|
|
|||
|
|
@ -7,43 +7,32 @@ using Elsa.Workflows.Management.Notifications;
|
|||
namespace Elsa.Workflows.Management.Services;
|
||||
|
||||
/// <inheritdoc />
|
||||
public class WorkflowDefinitionManager : IWorkflowDefinitionManager
|
||||
public class WorkflowDefinitionManager(
|
||||
IWorkflowDefinitionStore store,
|
||||
INotificationSender notificationSender,
|
||||
IWorkflowDefinitionPublisher workflowPublisher) : IWorkflowDefinitionManager
|
||||
{
|
||||
private readonly IWorkflowDefinitionStore _store;
|
||||
private readonly INotificationSender _notificationSender;
|
||||
private readonly IWorkflowDefinitionPublisher _workflowPublisher;
|
||||
private readonly IIdentityGenerator _identityGenerator;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor.
|
||||
/// </summary>
|
||||
public WorkflowDefinitionManager(
|
||||
IWorkflowDefinitionStore store,
|
||||
INotificationSender notificationSender,
|
||||
IWorkflowDefinitionPublisher workflowPublisher,
|
||||
IIdentityGenerator identityGenerator)
|
||||
{
|
||||
_store = store;
|
||||
_notificationSender = notificationSender;
|
||||
_workflowPublisher = workflowPublisher;
|
||||
_identityGenerator = identityGenerator;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<long> DeleteByDefinitionIdAsync(string definitionId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _notificationSender.SendAsync(new WorkflowDefinitionDeleting(definitionId), cancellationToken);
|
||||
var filter = new WorkflowDefinitionFilter { DefinitionId = definitionId };
|
||||
var count = await _store.DeleteAsync(filter, cancellationToken);
|
||||
await _notificationSender.SendAsync(new WorkflowDefinitionDeleted(definitionId), cancellationToken);
|
||||
await notificationSender.SendAsync(new WorkflowDefinitionDeleting(definitionId), cancellationToken);
|
||||
var filter = new WorkflowDefinitionFilter
|
||||
{
|
||||
DefinitionId = definitionId
|
||||
};
|
||||
var count = await store.DeleteAsync(filter, cancellationToken);
|
||||
await notificationSender.SendAsync(new WorkflowDefinitionDeleted(definitionId), cancellationToken);
|
||||
return count;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> DeleteByIdAsync(string id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var filter = new WorkflowDefinitionFilter { Id = id };
|
||||
var definition = await _store.FindAsync(filter, cancellationToken);
|
||||
var filter = new WorkflowDefinitionFilter
|
||||
{
|
||||
Id = id
|
||||
};
|
||||
var definition = await store.FindAsync(filter, cancellationToken);
|
||||
|
||||
if (definition == null)
|
||||
return false;
|
||||
|
|
@ -55,11 +44,15 @@ public class WorkflowDefinitionManager : IWorkflowDefinitionManager
|
|||
public async Task<long> BulkDeleteByDefinitionIdsAsync(IEnumerable<string> definitionIds, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var definitionIdList = definitionIds.Distinct().ToList();
|
||||
await _notificationSender.SendAsync(new WorkflowDefinitionsDeleting(definitionIdList), cancellationToken);
|
||||
var filter = new WorkflowDefinitionFilter { DefinitionIds = definitionIdList, IsReadonly = false };
|
||||
var count = await _store.DeleteAsync(filter, cancellationToken);
|
||||
await notificationSender.SendAsync(new WorkflowDefinitionsDeleting(definitionIdList), cancellationToken);
|
||||
var filter = new WorkflowDefinitionFilter
|
||||
{
|
||||
DefinitionIds = definitionIdList,
|
||||
IsReadonly = false
|
||||
};
|
||||
var count = await store.DeleteAsync(filter, cancellationToken);
|
||||
await EnsureLastVersionIsLatestAsync(definitionIdList, cancellationToken);
|
||||
await _notificationSender.SendAsync(new WorkflowDefinitionsDeleted(definitionIdList), cancellationToken);
|
||||
await notificationSender.SendAsync(new WorkflowDefinitionsDeleted(definitionIdList), cancellationToken);
|
||||
return count;
|
||||
}
|
||||
|
||||
|
|
@ -67,21 +60,31 @@ public class WorkflowDefinitionManager : IWorkflowDefinitionManager
|
|||
public async Task<long> BulkDeleteByIdsAsync(IEnumerable<string> ids, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var idList = ids.ToList();
|
||||
var definitions = await _store.FindSummariesAsync(new WorkflowDefinitionFilter { Ids = idList }, cancellationToken);
|
||||
var definitions = await store.FindSummariesAsync(new WorkflowDefinitionFilter
|
||||
{
|
||||
Ids = idList
|
||||
}, cancellationToken);
|
||||
var definitionIds = definitions.Select(x => x.DefinitionId).Distinct().ToList();
|
||||
await _notificationSender.SendAsync(new WorkflowDefinitionVersionsDeleting(idList), cancellationToken);
|
||||
var filter = new WorkflowDefinitionFilter { Ids = idList };
|
||||
var count = await _store.DeleteAsync(filter, cancellationToken);
|
||||
await notificationSender.SendAsync(new WorkflowDefinitionVersionsDeleting(idList), cancellationToken);
|
||||
var filter = new WorkflowDefinitionFilter
|
||||
{
|
||||
Ids = idList
|
||||
};
|
||||
var count = await store.DeleteAsync(filter, cancellationToken);
|
||||
await EnsureLastVersionIsLatestAsync(definitionIds, cancellationToken);
|
||||
await _notificationSender.SendAsync(new WorkflowDefinitionVersionsDeleted(idList), cancellationToken);
|
||||
await notificationSender.SendAsync(new WorkflowDefinitionVersionsDeleted(idList), cancellationToken);
|
||||
return count;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> DeleteVersionAsync(string definitionId, int versionToDelete, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var filter = new WorkflowDefinitionFilter { DefinitionId = definitionId, VersionOptions = VersionOptions.SpecificVersion(versionToDelete) };
|
||||
var definitionToDelete = await _store.FindAsync(filter, cancellationToken);
|
||||
var filter = new WorkflowDefinitionFilter
|
||||
{
|
||||
DefinitionId = definitionId,
|
||||
VersionOptions = VersionOptions.SpecificVersion(versionToDelete)
|
||||
};
|
||||
var definitionToDelete = await store.FindAsync(filter, cancellationToken);
|
||||
|
||||
if (definitionToDelete == null)
|
||||
return false;
|
||||
|
|
@ -94,42 +97,30 @@ public class WorkflowDefinitionManager : IWorkflowDefinitionManager
|
|||
{
|
||||
if (definitionToDelete.IsPublished)
|
||||
{
|
||||
await _workflowPublisher.RetractAsync(definitionToDelete, cancellationToken);
|
||||
await workflowPublisher.RetractAsync(definitionToDelete, cancellationToken);
|
||||
}
|
||||
|
||||
await _notificationSender.SendAsync(new WorkflowDefinitionVersionDeleting(definitionToDelete), cancellationToken);
|
||||
await notificationSender.SendAsync(new WorkflowDefinitionVersionDeleting(definitionToDelete), cancellationToken);
|
||||
|
||||
var filter = new WorkflowDefinitionFilter { Id = definitionToDelete.Id };
|
||||
var isDeleted = await _store.DeleteAsync(filter, cancellationToken) > 0;
|
||||
var filter = new WorkflowDefinitionFilter
|
||||
{
|
||||
Id = definitionToDelete.Id
|
||||
};
|
||||
var isDeleted = await store.DeleteAsync(filter, cancellationToken) > 0;
|
||||
|
||||
if (!isDeleted)
|
||||
return false;
|
||||
|
||||
await EnsureLastVersionIsLatestAsync(definitionToDelete.DefinitionId, cancellationToken);
|
||||
await _notificationSender.SendAsync(new WorkflowDefinitionVersionDeleted(definitionToDelete), cancellationToken);
|
||||
await notificationSender.SendAsync(new WorkflowDefinitionVersionDeleted(definitionToDelete), cancellationToken);
|
||||
|
||||
return isDeleted;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<WorkflowDefinition> RevertVersionAsync(string definitionId, int version, CancellationToken cancellationToken = default)
|
||||
public Task<WorkflowDefinition> RevertVersionAsync(string definitionId, int version, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var filter = new WorkflowDefinitionFilter { DefinitionId = definitionId, VersionOptions = VersionOptions.Latest };
|
||||
var latestVersion = await _store.FindAsync(filter, cancellationToken);
|
||||
|
||||
if (latestVersion != null)
|
||||
{
|
||||
latestVersion.IsLatest = false;
|
||||
await _store.SaveAsync(latestVersion, cancellationToken);
|
||||
}
|
||||
|
||||
var draft = await _workflowPublisher.GetDraftAsync(definitionId, VersionOptions.SpecificVersion(version), cancellationToken);
|
||||
draft!.Id = _identityGenerator.GenerateId();
|
||||
draft.Version = (latestVersion?.Version ?? 0) + 1;
|
||||
draft.IsLatest = true;
|
||||
|
||||
await _store.SaveAsync(draft, cancellationToken);
|
||||
return draft;
|
||||
return workflowPublisher.RevertVersionAsync(definitionId, version, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task EnsureLastVersionIsLatestAsync(IEnumerable<string> definitionIds, CancellationToken cancellationToken)
|
||||
|
|
@ -145,13 +136,16 @@ public class WorkflowDefinitionManager : IWorkflowDefinitionManager
|
|||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
private async Task EnsureLastVersionIsLatestAsync(string definitionId, CancellationToken cancellationToken)
|
||||
{
|
||||
var filter = new WorkflowDefinitionFilter { DefinitionId = definitionId };
|
||||
var lastVersion = await _store.FindLastVersionAsync(filter, cancellationToken);
|
||||
var filter = new WorkflowDefinitionFilter
|
||||
{
|
||||
DefinitionId = definitionId
|
||||
};
|
||||
var lastVersion = await store.FindLastVersionAsync(filter, cancellationToken);
|
||||
|
||||
if (lastVersion is null)
|
||||
return;
|
||||
|
||||
lastVersion.IsLatest = true;
|
||||
await _store.SaveAsync(lastVersion, cancellationToken);
|
||||
await store.SaveAsync(lastVersion, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
|
@ -62,7 +62,7 @@ public class WorkflowDefinitionPublisher(
|
|||
StringData = activitySerializer.Serialize(root),
|
||||
MaterializerName = JsonWorkflowMaterializer.MaterializerName
|
||||
};
|
||||
|
||||
|
||||
return Task.FromResult(workflowDefinition);
|
||||
}
|
||||
|
||||
|
|
@ -71,7 +71,7 @@ public class WorkflowDefinitionPublisher(
|
|||
{
|
||||
var filter = WorkflowDefinitionHandle.ByDefinitionId(definitionId, VersionOptions.Latest).ToFilter();
|
||||
var definition = await workflowDefinitionStore.FindAsync(filter, cancellationToken);
|
||||
|
||||
|
||||
if (definition == null)
|
||||
return new(false, new List<WorkflowValidationError>
|
||||
{
|
||||
|
|
@ -148,6 +148,30 @@ public class WorkflowDefinitionPublisher(
|
|||
return definition;
|
||||
}
|
||||
|
||||
public async Task<WorkflowDefinition> RevertVersionAsync(string definitionId, int version, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var filter = new WorkflowDefinitionFilter
|
||||
{
|
||||
DefinitionId = definitionId,
|
||||
VersionOptions = VersionOptions.Latest
|
||||
};
|
||||
var latestVersion = await workflowDefinitionStore.FindAsync(filter, cancellationToken);
|
||||
|
||||
if (latestVersion != null)
|
||||
{
|
||||
latestVersion.IsLatest = false;
|
||||
await workflowDefinitionStore.SaveAsync(latestVersion, cancellationToken);
|
||||
}
|
||||
|
||||
var draft = await GetDraftAsync(definitionId, VersionOptions.SpecificVersion(version), cancellationToken);
|
||||
draft!.Id = identityGenerator.GenerateId();
|
||||
draft.Version = (latestVersion?.Version ?? 0) + 1;
|
||||
draft.IsLatest = true;
|
||||
|
||||
await workflowDefinitionStore.SaveAsync(draft, cancellationToken);
|
||||
return draft;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<WorkflowDefinition?> GetDraftAsync(string definitionId, VersionOptions versionOptions, CancellationToken cancellationToken = default)
|
||||
{
|
||||
|
|
@ -199,7 +223,7 @@ public class WorkflowDefinitionPublisher(
|
|||
await workflowDefinitionStore.SaveAsync(draft, cancellationToken);
|
||||
await mediator.SendAsync(new WorkflowDefinitionDraftSaved(draft), cancellationToken);
|
||||
|
||||
if (lastVersion is null)
|
||||
if (lastVersion is null)
|
||||
await mediator.SendAsync(new WorkflowDefinitionCreated(definition), cancellationToken);
|
||||
|
||||
if (lastVersion is { IsPublished: true, IsLatest: true })
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ public static class DependencyInjectionExtensions
|
|||
/// <param name="services">Service collection</param>
|
||||
/// <typeparam name="TValidator">Validator of the validator</typeparam>
|
||||
/// <typeparam name="TPayload">Payload type</typeparam>
|
||||
public static IServiceCollection AddTriggerPaylodValidator<TValidator, TPayload>(this IServiceCollection services)
|
||||
public static IServiceCollection AddTriggerPayloadValidator<TValidator, TPayload>(this IServiceCollection services)
|
||||
where TValidator : class, ITriggerPayloadValidator<TPayload>
|
||||
{
|
||||
return services.AddScoped<ITriggerPayloadValidator<TPayload>, TValidator>();
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -32,7 +32,10 @@ public class FlowchartNextActivityTests
|
|||
await _services.PopulateRegistriesAsync();
|
||||
await _workflowRunner.RunAsync<FlowchartWorkflow>();
|
||||
var lines = _capturingTextWriter.Lines.ToList();
|
||||
Assert.Equal(new[] { "Line 1" }, lines);
|
||||
Assert.Equal(new[]
|
||||
{
|
||||
"Line 1"
|
||||
}, lines);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Flowchart with backward connections and a dangling activity")]
|
||||
|
|
@ -44,74 +47,71 @@ public class FlowchartNextActivityTests
|
|||
|
||||
var start = new Start();
|
||||
var dangling = new WriteLine("dangling");
|
||||
var writeLineDecision = new FlowSwitch()
|
||||
var writeLineDecision = new FlowSwitch
|
||||
{
|
||||
Cases = {
|
||||
new FlowSwitchCase("LessThanThree", new Expression("JavaScript", "getVariable('LoopCount') < 3")),
|
||||
new FlowSwitchCase("LessThanOne", new Expression("JavaScript", "getVariable('LoopCount') < 1")),
|
||||
},
|
||||
Cases =
|
||||
{
|
||||
new("LessThanThree", new Expression("JavaScript", "getVariable('LoopCount') < 3"))
|
||||
},
|
||||
Mode = new(SwitchMode.MatchAny)
|
||||
};
|
||||
var a = new WriteLine("A");
|
||||
var b = new WriteLine("B");
|
||||
var c = new WriteLine("C");
|
||||
var incrementLoop = new SetVariable()
|
||||
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()
|
||||
var loopbackDecision = new FlowSwitch
|
||||
{
|
||||
Cases = {
|
||||
new FlowSwitchCase("EqualOne", new Expression("JavaScript", "getVariable('LoopCount') == 1")),
|
||||
new FlowSwitchCase("LessThanFour", new Expression("JavaScript", "getVariable('LoopCount') < 4")),
|
||||
},
|
||||
Mode = new(SwitchMode.MatchFirst)
|
||||
Cases =
|
||||
{
|
||||
new("EqualOne", new Expression("JavaScript", "getVariable('LoopCount') == 1")),
|
||||
new("LessThanFour", new Expression("JavaScript", "getVariable('LoopCount') < 4")),
|
||||
new("EqualThree", new Expression("JavaScript", "getVariable('LoopCount') == 3")),
|
||||
},
|
||||
Mode = new(SwitchMode.MatchAny)
|
||||
};
|
||||
var d = new WriteLine("D");
|
||||
var e = new WriteLine("E");
|
||||
var f = new WriteLine("F");
|
||||
var end = new End();
|
||||
|
||||
|
||||
workflowBuilder.Root = new Flowchart
|
||||
{
|
||||
Variables =
|
||||
{
|
||||
loopVariable
|
||||
},
|
||||
{
|
||||
loopVariable
|
||||
},
|
||||
Activities =
|
||||
{
|
||||
start,
|
||||
dangling,
|
||||
writeLineDecision,
|
||||
a,
|
||||
b,
|
||||
c,
|
||||
incrementLoop,
|
||||
loopbackDecision,
|
||||
d,
|
||||
e,
|
||||
f,
|
||||
end
|
||||
},
|
||||
{
|
||||
start,
|
||||
dangling,
|
||||
writeLineDecision,
|
||||
a,
|
||||
b,
|
||||
incrementLoop,
|
||||
loopbackDecision,
|
||||
d,
|
||||
e,
|
||||
f,
|
||||
end
|
||||
},
|
||||
Connections =
|
||||
{
|
||||
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, "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, "EqualThree"), new Endpoint(f)),
|
||||
new(f, end),
|
||||
}
|
||||
};
|
||||
|
|
@ -121,7 +121,10 @@ public class FlowchartNextActivityTests
|
|||
var result = await _workflowRunner.RunAsync(workflow);
|
||||
var lines = _capturingTextWriter.Lines.ToList();
|
||||
Assert.Equal(WorkflowSubStatus.Finished, result.WorkflowState.SubStatus);
|
||||
Assert.Equal(new[] { "A", "B", "C", "D", "E", "A", "B", "E", "F" }, lines);
|
||||
Assert.Equal(new[]
|
||||
{
|
||||
"A", "B", "D", "E", "A", "B", "E", "E", "F"
|
||||
}, lines);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Flowchart with an invalid backward connection")]
|
||||
|
|
@ -132,24 +135,42 @@ public class FlowchartNextActivityTests
|
|||
|
||||
var workflow = new TestWorkflow(workflowBuilder =>
|
||||
{
|
||||
var start = new Start() { Id = "Start" };
|
||||
var a = new WriteLine("A") { Id = "WriteLineA" };
|
||||
var b = new WriteLine("B") { Id = "WriteLineB" };
|
||||
var c = new WriteLine("C") { Id = "WriteLineC" };
|
||||
var d = new WriteLine("D") { Id = "WriteLineD" };
|
||||
var e = new WriteLine("E") { Id = "WriteLineE" };
|
||||
var start = new Start
|
||||
{
|
||||
Id = "Start"
|
||||
};
|
||||
var a = new WriteLine("A")
|
||||
{
|
||||
Id = "WriteLineA"
|
||||
};
|
||||
var b = new WriteLine("B")
|
||||
{
|
||||
Id = "WriteLineB"
|
||||
};
|
||||
var c = new WriteLine("C")
|
||||
{
|
||||
Id = "WriteLineC"
|
||||
};
|
||||
var d = new WriteLine("D")
|
||||
{
|
||||
Id = "WriteLineD"
|
||||
};
|
||||
var e = new WriteLine("E")
|
||||
{
|
||||
Id = "WriteLineE"
|
||||
};
|
||||
|
||||
workflowBuilder.Root = new Flowchart
|
||||
{
|
||||
Activities =
|
||||
{
|
||||
start,
|
||||
a,
|
||||
b,
|
||||
c,
|
||||
d,
|
||||
e,
|
||||
},
|
||||
{
|
||||
start,
|
||||
a,
|
||||
b,
|
||||
c,
|
||||
d,
|
||||
e,
|
||||
},
|
||||
Connections =
|
||||
{
|
||||
new(start, a),
|
||||
|
|
@ -169,7 +190,10 @@ public class FlowchartNextActivityTests
|
|||
Assert.Equal(WorkflowSubStatus.Faulted, result.WorkflowState.SubStatus);
|
||||
Assert.Equal(1, result.WorkflowState.Incidents.Count());
|
||||
Assert.Equal("Invalid backward connection: Every path from the source ('WriteLineE') must go through the target ('WriteLineC') when tracing back to the start.", result.WorkflowState.Incidents.First().Message);
|
||||
Assert.Equal(new[] { "A", "B", "C", "D", "E" }, lines);
|
||||
Assert.Equal(new[]
|
||||
{
|
||||
"A", "B", "C", "D", "E"
|
||||
}, lines);
|
||||
}
|
||||
|
||||
[Theory(DisplayName = "Flowchart with a Join activity executed multiple times")]
|
||||
|
|
@ -186,43 +210,42 @@ public class FlowchartNextActivityTests
|
|||
var b = new WriteLine("B");
|
||||
var c = new WriteLine("C");
|
||||
var d = new WriteLine("D");
|
||||
var join = new FlowJoin()
|
||||
var join = new FlowJoin
|
||||
{
|
||||
Mode = new(joinMode)
|
||||
};
|
||||
var incrementLoop = new SetVariable()
|
||||
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()
|
||||
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 =
|
||||
{
|
||||
loopVariable
|
||||
},
|
||||
{
|
||||
loopVariable
|
||||
},
|
||||
Activities =
|
||||
{
|
||||
start,
|
||||
a,
|
||||
b,
|
||||
c,
|
||||
d,
|
||||
join,
|
||||
incrementLoop,
|
||||
loopbackDecision,
|
||||
end
|
||||
},
|
||||
{
|
||||
start,
|
||||
a,
|
||||
b,
|
||||
c,
|
||||
d,
|
||||
join,
|
||||
incrementLoop,
|
||||
loopbackDecision,
|
||||
end
|
||||
},
|
||||
Connections =
|
||||
{
|
||||
new(start, a),
|
||||
|
|
@ -234,8 +257,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 +268,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")]
|
||||
|
|
@ -258,49 +280,48 @@ public class FlowchartNextActivityTests
|
|||
var loopVariable = new Variable<int>("LoopCount", 0);
|
||||
|
||||
var start = new Start();
|
||||
var loopbackSwitch = new FlowSwitch()
|
||||
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)
|
||||
};
|
||||
var a = new WriteLine("A");
|
||||
var incrementLoop = new SetVariable()
|
||||
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()
|
||||
var join = new FlowJoin
|
||||
{
|
||||
Mode = new(joinMode)
|
||||
};
|
||||
var b = new WriteLine("B");
|
||||
var end = new End();
|
||||
|
||||
|
||||
|
||||
workflowBuilder.Root = new Flowchart
|
||||
{
|
||||
Variables =
|
||||
{
|
||||
loopVariable
|
||||
},
|
||||
{
|
||||
loopVariable
|
||||
},
|
||||
Activities =
|
||||
{
|
||||
start,
|
||||
loopbackSwitch,
|
||||
a,
|
||||
incrementLoop,
|
||||
join,
|
||||
b,
|
||||
end
|
||||
},
|
||||
{
|
||||
start,
|
||||
loopbackSwitch,
|
||||
a,
|
||||
incrementLoop,
|
||||
join,
|
||||
b,
|
||||
end
|
||||
},
|
||||
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),
|
||||
|
|
@ -313,6 +334,9 @@ public class FlowchartNextActivityTests
|
|||
var result = await _workflowRunner.RunAsync(workflow);
|
||||
var lines = _capturingTextWriter.Lines.ToList();
|
||||
Assert.Equal(WorkflowSubStatus.Finished, result.WorkflowState.SubStatus);
|
||||
Assert.Equal(new[] { "A", "A", "A", "B" }, lines);
|
||||
Assert.Equal(new[]
|
||||
{
|
||||
"A", "A", "A", "B"
|
||||
}, 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 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