elsa-workflows/elsa-core · error · Exception
Invalid backward connection: Every path from the source
Error message
Invalid backward connection: Every path from the source ('{outboundConnection.Source.Activity.Id}') must go through the target ('{outboundConnection.Target.Activity.Id}') when tracing back to the start. What it means
A backward connection (loop-back edge) is only valid if every path from its source activity back to the flowchart start passes through the connection's target — i.e. the target acts as the loop's re-entry point. When a detected backward edge violates this structural rule, MaybeScheduleBackwardConnectionActivityAsync throws InvalidOperationException because loop counter bookkeeping would be incorrect.
Solutions
- Rewire the backward connection so its target is the loop head — the node through which every path from source to Start passes.
- Restructure the loop so all branches converge at a single join activity before looping back.
- Validate the graph before running: trace paths from the connection source to Start and confirm they include the target.
- If the edge is not a loop, re-draw it as a forward connection to the correct successor.
Example fix
// before: loop-back drawn from Decision to a mid-body node (not on all paths) new Connection(decision, midBody) // after: loop back to the loop-head join that every path from decision passes through new Connection(decision, loopHeadJoin)
Defensive patterns
Strategy: validation
Validate before calling
foreach (var c in flowchart.Connections.Where(IsBackward))
{
bool valid = AllPathsFromSourceToStartPassThrough(c.Source.Activity, c.Target.Activity, flowchart);
if (!valid) throw new InvalidOperationException($"Backward connection from '{c.Source.Activity.Id}' must target the loop head '{c.Target.Activity.Id}'.");
} Try / catch
try { await MaybeScheduleBackwardConnectionActivityAsync(...); }
catch (InvalidOperationException ex) when (ex.Message.StartsWith("Invalid backward connection:"))
{
logger.LogError(ex, "Workflow contains an invalid loop-back connection.");
} Prevention
- Model loops so every path from the loop-back source to Start goes through a single join/loop-head activity.
- Validate backward connections after editing flows in the designer.
- Prefer dedicated loop constructs (For/While/WhileEach) over hand-drawn backward edges where possible.
When it happens
Trigger: Designer workflows where a loop-back connection targets a node that is not on every path from source to start, e.g. multiple parallel branches merging back at different points, or a backward edge drawn from a mid-branch node to a node outside the loop body.
Common situations: Complex loops built by hand in the designer; refactoring a flowchart that removed the intended loop-head node; copies of flows where connection endpoints shifted after node deletion.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- Source label ' ' not found in flowchart
- Target label ' ' not found in flowchart
- Activity context is not a flowchart.
- Failed to parse JsonDocument
- Missing property ' ' or
AI-assisted analysis of elsa-workflows/elsa-core@fe9217bdfa (2026-09-13).
Data as JSON: /api/errors/099fb59e261f7a87.
Report an issue: GitHub.
Appendix: source
Thrown at src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.Counters.cs:217
hasScheduledActivity |= await MaybeScheduleOutboundActivityAsync(flowGraph, flowScope, flowchartContext, completedActivityContext, outboundConnection, outboundActivity, completionCallback);
}
return hasScheduledActivity;
}
/// <summary>
/// Schedules an outbound activity that originates from a backward connection.
/// </summary>
private static async ValueTask<bool> MaybeScheduleBackwardConnectionActivityAsync(FlowGraph flowGraph, ActivityExecutionContext flowchartContext, ActivityExecutionContext? completedActivityContext, Connection outboundConnection, IActivity outboundActivity, bool connectionFollowed, bool backwardConnectionIsValid, ActivityCompletionCallback completionCallback)
{
if (!connectionFollowed)
{
return false;
}
if (!backwardConnectionIsValid)
{
throw new($"Invalid backward connection: Every path from the source ('{outboundConnection.Source.Activity.Id}') must go through the target ('{outboundConnection.Target.Activity.Id}') when tracing back to the start.");
}
var scheduleWorkOptions = new ScheduleWorkOptions
{
CompletionCallback = completionCallback,
Input = new Dictionary<string, object>() { { BackwardConnectionActivityInput, true } },
SchedulingActivityExecutionId = completedActivityContext?.Id
};
await flowchartContext.ScheduleActivityAsync(outboundActivity, scheduleWorkOptions);
return true;
}
/// <summary>
/// Determines the merge mode for a given outbound activity. If the outbound activity is a FlowJoin, it retrieves its configured
/// mode. Otherwise, it defaults to FlowJoinMode.WaitAllActive for implicit joins.
/// </summary>
private static async ValueTask<FlowJoinMode> GetMergeModeAsync(ActivityExecutionContext flowchartContext, IActivity outboundActivity)View on GitHub (pinned to fe9217bdfa)