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

  1. Rewire the backward connection so its target is the loop head — the node through which every path from source to Start passes.
  2. Restructure the loop so all branches converge at a single join activity before looping back.
  3. Validate the graph before running: trace paths from the connection source to Start and confirm they include the target.
  4. 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

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


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)