flowable/flowable-engine · error · PvmException

joining scope executions is not allowed

Error message

joining scope executions is not allowed

What it means

A PvmException thrown by ExecutionImpl.takeAll when more than one execution is being recycled/joined at an activity and at least one of them is a scope execution. Scope executions (e.g. those owning process/scope variables or sub-scopes) may not be joined together. This guards the execution-tree scope invariants during concurrent join (e.g. parallel gateway merge).

Solutions

  1. Redesign the custom join behavior to only recycle non-scope concurrent (leaf) executions
  2. Move the join out of the sub-process scope so participating executions are not scope roots
  3. Use the engine's built-in parallel/inclusive gateway instead of custom takeAll logic
  4. Check whether the joined executions became scopes due to async boundaries/event scopes and adjust the model

Example fix

// before
List<ActivityExecution> joined = executions.stream()
    .filter(e -> ((ExecutionImpl) e).isScope() || true) // recycles scopes too
    .collect(toList());
takeAll(joined, null);
// after
List<ActivityExecution> leaves = executions.stream()
    .filter(e -> !((ExecutionImpl) e).isScope())
    .collect(toList());
takeAll(leaves, null);
Defensive patterns

Strategy: validation

Validate before calling

// before takeAll, verify no recyclable execution is a scope
boolean scopeJoin = recyclable.stream().anyMatch(e -> ((ExecutionImpl) e).isScope());
if (scopeJoin) throw new ActivitiException("cannot join scope executions");

Type guard

boolean isLeaf(PvmExecution e) { return e instanceof ExecutionImpl && !((ExecutionImpl) e).isScope(); }

Try / catch

try {
  customJoin.executions(recyclable);
} catch (PvmException e) {
  if (e.getMessage().contains("joining scope executions")) { /* redesign join */ }
}

Prevention

When it happens

Trigger: takeAll() called with multiple recyclable executions where isScope() is true for one of them — typically from custom concurrency behaviors attempting to join executions that own scopes (embedded sub-process scope roots) instead of leaf concurrent executions.

Common situations: Custom join/merge behaviors incorrectly recycling scope-rooted executions; modeling constructs that fork inside an embedded sub-process and join at scope level; engine-version migrations where scope semantics changed.

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 flowable/flowable-engine@d6d39ce1c6 (2026-09-11). Data as JSON: /api/errors/296e0f183adb01ad. Report an issue: GitHub.

Appendix: source

Thrown at modules/flowable5-engine/src/main/java/org/activiti/engine/impl/pvm/runtime/ExecutionImpl.java:641

            }
        }
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("inactive concurrent executions in '{}': {}", activity, inactiveConcurrentExecutionsInActivity);
            LOGGER.debug("other concurrent executions: {}", otherConcurrentExecutions);
        }
        return inactiveConcurrentExecutionsInActivity;
    }

    @Override
    @SuppressWarnings("unchecked")
    public void takeAll(List<PvmTransition> transitions, List<ActivityExecution> recyclableExecutions) {
        transitions = new ArrayList<>(transitions);
        recyclableExecutions = (recyclableExecutions != null ? new ArrayList<>(recyclableExecutions) : new ArrayList<>());

        if (recyclableExecutions.size() > 1) {
            for (ActivityExecution recyclableExecution : recyclableExecutions) {
                if (((ExecutionImpl) recyclableExecution).isScope()) {
                    throw new PvmException("joining scope executions is not allowed");
                }
            }
        }

        ExecutionImpl concurrentRoot = ((isConcurrent && !isScope) ? getParent() : this);
        List<ExecutionImpl> concurrentActiveExecutions = new ArrayList<>();
        for (ExecutionImpl execution : concurrentRoot.getExecutions()) {
            if (execution.isActive()) {
                concurrentActiveExecutions.add(execution);
            }
        }

        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("transitions to take concurrent: {}", transitions);
            LOGGER.debug("active concurrent executions: {}", concurrentActiveExecutions);
        }

        if ((transitions.size() == 1)

View on GitHub (pinned to d6d39ce1c6)