flowable/flowable-engine · error · FlowableException

Unbound boundary event execution prevents the sub process in

Error message

Unbound boundary event execution prevents the sub process instance to be moved " + subProcessExecution.getId()

What it means

Thrown by AbstractDynamicStateManager when moving a process instance to a different process definition (process instance migration/upgrade). Before deleting the old sub process instance executions, the engine validates that every execution sitting on a BoundaryEvent has its parent execution included in the set of execution IDs being moved. If a boundary-event execution is 'unbound' (no parent, or parent not part of the migration set), the migration would orphan it, so the engine refuses.

Source

Thrown at modules/flowable-engine/src/main/java/org/flowable/engine/impl/dynamic/AbstractDynamicStateManager.java:717

    protected abstract boolean isDirectFlowElementExecutionMigration(FlowElement currentFlowElement, FlowElement newFlowElement);

    protected void safeDeleteSubProcessInstance(String processInstanceId, List<ExecutionEntity> executionsPool, String deleteReason, CommandContext commandContext) {
        ExecutionEntityManager executionEntityManager = CommandContextUtil.getExecutionEntityManager(commandContext);

        //Confirm that all the subProcessExecutions are in the executions pool
        List<ExecutionEntity> subProcessExecutions = executionEntityManager.findChildExecutionsByProcessInstanceId(processInstanceId);
        
        Set<String> executionIdsToMove = new HashSet<>();
        for (ExecutionEntity executionPoolItem : executionsPool) {
            executionIdsToMove.add(executionPoolItem.getId());
        }

        for (ExecutionEntity subProcessExecution : subProcessExecutions) {
            FlowElement currentFlowElement = subProcessExecution.getCurrentFlowElement();
            if (currentFlowElement != null && currentFlowElement instanceof BoundaryEvent) {
                String parentExecutionId = subProcessExecution.getParentId();
                if (!StringUtils.isNotEmpty(parentExecutionId) || !executionIdsToMove.contains(parentExecutionId)) {
                    throw new FlowableException("Unbound boundary event execution prevents the sub process instance to be moved " + subProcessExecution.getId());
                }
            
            } else if (!executionIdsToMove.contains(subProcessExecution.getId())) {
                throw new FlowableException("Following execution of sub process instance is not moved " + subProcessExecution.getId());
            }
        }

        // delete the sub process instance
        executionEntityManager.deleteProcessInstance(processInstanceId, deleteReason, true);
    }

    protected ExecutionEntity deleteParentExecutions(String parentExecutionId, Collection<FlowElementMoveEntry> moveToFlowElements, CommandContext commandContext) {
        return deleteParentExecutions(parentExecutionId, moveToFlowElements, null, commandContext);
    }

    protected ExecutionEntity deleteParentExecutions(String parentExecutionId, Collection<FlowElementMoveEntry> moveToFlowElements, Collection<String> executionIdsNotToDelete, CommandContext commandContext) {
        ExecutionEntityManager executionEntityManager = CommandContextUtil.getExecutionEntityManager(commandContext);

View on GitHub (pinned to d6d39ce1c6)

Solutions

  1. Add the boundary event's activity (and its parent subprocess activity) to the migration activity mapping, e.g. .addActivityMapping("oldBoundaryTask", "newBoundaryTask")
  2. Map the parent subprocess execution so executionIdsToMove contains the parent of the boundary-event execution
  3. Enable a mapped boundary event in the target definition so the execution can be moved, or cancel/complete the boundary wait state before migrating
  4. Migrate in a state where no boundary-event executions are active (e.g. after timers fire or messages arrive)

Example fix

// before
migrationBuilder.migrateTo(newDefId);
// after
migrationBuilder.addActivityMapping("subProcessTask", "subProcessTask")
    .addActivityMapping("boundaryUserTask", "boundaryUserTask")
    .migrateTo(newDefId);
Defensive patterns

Strategy: validation

Validate before calling

List<Execution> execs = runtimeService.createExecutionQuery().processInstanceId(pid).list();
boolean unbound = execs.stream().anyMatch(e ->
    e.getCurrentFlowElement() instanceof BoundaryEvent &&
    !mappedExecutionIds.contains(e.getParentId()));
if (unbound) throw new IllegalStateException("Map boundary-event parent executions before migrating");

Type guard

static boolean isMappedBoundary(ExecutionEntity e, Set<String> ids) {
    return !(e.getCurrentFlowElement() instanceof BoundaryEvent) || ids.contains(e.getParentId());
}

Try / catch

try {
    migrationBuilder.migrateTo(newDefId);
} catch (FlowableException ex) {
    if (ex.getMessage().startsWith("Unbound boundary event execution")) {
        // augment activity mappings and retry
    } else throw ex;
}

Prevention

When it happens

Trigger: Calling ProcessInstanceMigrationBuilder.migrateTo(processDefinitionId) / ProcessMigrationService process-instance migration while a subprocess in the instance has an active boundary-event execution whose parent execution is not mapped in the migration activity mapping.

Common situations: Migrating instances that are waiting on a boundary timer/message/signal inside an embedded or call-activity subprocess without mapping the boundary event or its parent activity in the migration map; partial activity mappings that miss subprocess-internal executions.

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