flowable/flowable-engine · error · FlowableIllegalStateException

plan item instance is already suspended

Error message

plan item instance is already suspended

What it means

SuspendPlanItemInstanceOperation.isStateNotChanged detects idempotent re-suspension: if the plan item instance's old state already equals the new state (already suspended), suspending it again is illegal and a FlowableIllegalStateException is thrown instead of silently repeating the operation.

Source

Thrown at modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/agenda/operation/SuspendPlanItemInstanceOperation.java:64

        planItemInstanceEntity.setLastSuspendedTime(getCurrentTime(commandContext));
        
        PlanItemDefinition planItemDefinition = planItemInstanceEntity.getPlanItem().getPlanItemDefinition();
        if (planItemDefinition instanceof TimerEventListener) {
            CmmnEngineConfiguration cmmnEngineConfiguration = CommandContextUtil.getCmmnEngineConfiguration(commandContext);
            List<TimerJobEntity> timerJobs = cmmnEngineConfiguration.getJobServiceConfiguration().getTimerJobEntityManager().findJobsByScopeIdAndSubScopeId(
                    planItemInstanceEntity.getCaseInstanceId(), planItemInstanceEntity.getId());
            if (timerJobs != null && !timerJobs.isEmpty()) {
                cmmnEngineConfiguration.getJobServiceConfiguration().getJobService().moveJobToSuspendedJob(timerJobs.get(0));
            }
        }
        
        CommandContextUtil.getCmmnHistoryManager(commandContext).recordPlanItemInstanceSuspended(planItemInstanceEntity);
    }
    
    @Override
    public boolean isStateNotChanged(String oldState, String newState) {
        if (oldState != null && oldState.equals(newState)) {
            throw new FlowableIllegalStateException("plan item instance is already suspended");
        }
        
        return false;
    }

    @Override
    public boolean abortOperationIfNewStateEqualsOldState() {
        return true;
    }

    @Override
    public String getOperationName() {
        return "[Suspend plan item]";
    }
}

View on GitHub (pinned to d6d39ce1c6)

Solutions

  1. Query the plan item instance and skip suspension when state is already 'suspended'.
  2. Make suspend actions idempotent at the application layer (guard with state check or lock).
  3. Catch FlowableIllegalStateException and treat it as success/no-op when appropriate.
  4. Avoid suspending a container whose children are partially suspended without checking states.

Example fix

// before
cmmnRuntimeService.suspendPlanItemInstance(planItemInstanceId);
// after
PlanItemInstance pii = cmmnRuntimeService.createPlanItemInstanceQuery()
    .planItemInstanceId(planItemInstanceId).singleResult();
if (pii != null && !"suspended".equals(pii.getState())) {
    cmmnRuntimeService.suspendPlanItemInstance(planItemInstanceId);
}
Defensive patterns

Strategy: validation

Validate before calling

// java
PlanItemInstance pii = cmmnRuntimeService.createPlanItemInstanceQuery()
    .planItemInstanceId(planItemInstanceId).singleResult();
if (pii == null || "suspended".equals(pii.getState())) return; // nothing to do

Type guard

boolean needsSuspend(PlanItemInstance pii) {
    return pii != null && !PlanItemInstanceState.SUSPENDED.equals(pii.getState());
}

Try / catch

try {
    cmmnRuntimeService.suspendPlanItemInstance(id);
} catch (FlowableIllegalStateException e) {
    if (e.getMessage().contains("already suspended")) {
        // idempotent no-op
    }
}

Prevention

When it happens

Trigger: Calling suspendPlanItemInstance (or suspending a case whose item is already suspended) for a plan item instance whose state is already 'suspended'.

Common situations: Double-invocation from retries or UI double-click; suspending a whole case tree where an item was already suspended in a prior partial suspension; concurrent suspend requests.

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