elsa-workflows/elsa-core · error · InvalidOperationException

BPMN element ' ' binds process activity ' ' as the work of…

Error message

BPMN element '{start.ElementId}' binds process activity '{nested.Id}' as the work of scope '{process.Id}', but that activity declares itself the workflow's root scope. A nested scope's start events are internal to the process around it, not workflow entry points, so it must not be marked as able to start the workflow.

What it means

BpmnCommandApplier.ApplyAsync pre-scans StartWork commands and refuses a command that binds a process activity marked as the workflow's root scope (IsRootScope) as nested work of another scope. Nested scopes' start events are internal and must never act as workflow entry points. It throws InvalidOperationException naming the BPMN element, the bound activity, and the owning scope.

Solutions

  1. Fix the model so the referenced process is not marked as root scope but is genuinely nested.
  2. Regenerate the command batch so StartWork only binds non-root child activities.
  3. Check the version alignment between the interpreter producing commands and the host applying them.
Defensive patterns

Strategy: try-catch

Validate before calling

foreach (var c in commands.OfType<BpmnHostCommand.StartWork>())
    if (process.FindWorkActivity(c.BindingRef) is BpmnProcess { IsRootScope: true }) throw new InvalidOperationException("Batch binds a root scope as nested work.");

Try / catch

try { await applier.ApplyAsync(context, commands); }
catch (InvalidOperationException ex) when (ex.Message.Contains("root scope")) { log.LogError(ex, "Invalid StartWork binding"); throw; }

Prevention

When it happens

Trigger: Applying a host command batch (via ApplyAsync) containing BpmnHostCommand.StartWork whose BindingRef resolves to a BpmnProcess with IsRootScope == true while nested under another process.

Common situations: Corrupted or hand-built command batches; a bug in the interpreter's binding derivation; model where a root process is incorrectly referenced as a subprocess of another process.

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/ad4f7b51e2207f89. Report an issue: GitHub.

Appendix: source

Thrown at src/modules/Elsa.Bpmn/Hosting/BpmnCommandApplier.cs:44

    /// Applies a command list <b>in the order returned</b>.
    /// </summary>
    /// <remarks>
    /// The ordering carries meaning and is not an implementation detail. An interrupting boundary event emits the
    /// boundary path's <c>StartWork</c> <i>before</i> the teardown that retires the host it interrupted, and a host
    /// that tidied up first would be applying a different process.
    /// </remarks>
    public async ValueTask ApplyAsync(IReadOnlyList<BpmnHostCommand> commands)
    {
        // Refused before anything in the batch is applied. Applying commands one at a time and refusing only once
        // the offending StartWork is reached would leave earlier commands in the same batch already applied and
        // saved via memory.SaveWork() below — and under ContinueWithIncidentsStrategy that throw is absorbed into
        // an incident rather than surfaced, so the workflow would carry on with a half-applied batch and half-saved
        // memory instead of the refusal stopping it clean.
        foreach (var start in commands.OfType<BpmnHostCommand.StartWork>())
        {
            if (process.FindWorkActivity(start.BindingRef) is BpmnProcess { IsRootScope: true } nested)
            {
                throw new InvalidOperationException(
                    $"BPMN element '{start.ElementId}' binds process activity '{nested.Id}' as the work of scope '{process.Id}', but that activity declares itself the workflow's root scope. "
                    + "A nested scope's start events are internal to the process around it, not workflow entry points, so it must not be marked as able to start the workflow.");
            }
        }

        foreach (var command in commands)
        {
            switch (command)
            {
                case BpmnHostCommand.StartWork start:
                    await StartWorkAsync(start);
                    break;
                case BpmnHostCommand.CancelWorkSubtree cancel:
                    await CancelWorkSubtreeAsync(cancel);
                    break;
                case BpmnHostCommand.SignalEnclosingScope signal:
                    await SignalEnclosingScopeAsync(signal);
                    break;

View on GitHub (pinned to fe9217bdfa)