flowable/flowable-engine · error · FlowableIllegalStateException

plan item instance can only be resumed if the state is suspe

Error message

plan item instance can only be resumed if the state is suspended

What it means

ResumePlanItemInstanceOperation.isStateNotChanged asserts that a plan item instance is only resumed from the SUSPENDED state. If the recorded old state is non-null and anything other than SUSPENDED, resuming is an illegal transition and a FlowableIllegalStateException is thrown.

Source

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

    protected void internalExecute() {
        planItemInstanceEntity.setLastAvailableTime(getCurrentTime(commandContext));
        
        PlanItemDefinition planItemDefinition = planItemInstanceEntity.getPlanItem().getPlanItemDefinition();
        if (planItemDefinition instanceof TimerEventListener) {
            CmmnEngineConfiguration cmmnEngineConfiguration = CommandContextUtil.getCmmnEngineConfiguration(commandContext);
            List<SuspendedJobEntity> suspendedJobs = cmmnEngineConfiguration.getJobServiceConfiguration().getSuspendedJobEntityManager().findJobsBySubScopeId(planItemInstanceEntity.getId());
            if (suspendedJobs != null && !suspendedJobs.isEmpty()) {
                cmmnEngineConfiguration.getJobServiceConfiguration().getJobService().activateSuspendedJob(suspendedJobs.get(0));
            }
        }
        
        CommandContextUtil.getCmmnHistoryManager(commandContext).recordPlanItemInstanceAvailable(planItemInstanceEntity);
    }
    
    @Override
    public boolean isStateNotChanged(String oldState, String newState) {
        if (oldState != null && !PlanItemInstanceState.SUSPENDED.equals(oldState)) {
            throw new FlowableIllegalStateException("plan item instance can only be resumed if the state is suspended");
        }
        
        return oldState != null && oldState.equals(newState) && abortOperationIfNewStateEqualsOldState();
    }
    
    @Override
    public boolean abortOperationIfNewStateEqualsOldState() {
        return true;
    }

    @Override
    public String getOperationName() {
        return null; // Default one is ok.
    }
    
}

View on GitHub (pinned to d6d39ce1c6)

Solutions

  1. Check planItemInstance.getState() equals 'suspended' before resuming.
  2. If resuming a case, note that only suspended children are affected and avoid resuming non-suspended items individually.
  3. Serialize suspension/resume operations so concurrent agenda operations cannot change state in between.
  4. Catch FlowableIllegalStateException and treat it as a no-op if the item is already active.

Example fix

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

Strategy: validation

Validate before calling

// java
PlanItemInstance pii = cmmnRuntimeService.createPlanItemInstanceQuery()
    .planItemInstanceId(planItemInstanceId).singleResult();
if (pii == null || !"suspended".equals(pii.getState()))
    throw new IllegalStateException("Plan item not suspended: " + planItemInstanceId);

Type guard

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

Try / catch

try {
    cmmnRuntimeService.resumePlanItemInstance(id);
} catch (FlowableIllegalStateException e) {
    if (e.getMessage().contains("can only be resumed if the state is suspended")) {
        // already resumed or wrong state: treat as no-op
    }
}

Prevention

When it happens

Trigger: Triggering a resume operation (directly or via plan item / case resume APIs) on a plan item instance whose current state is e.g. AVAILABLE, ACTIVE, COMPLETED, or EXITED instead of SUSPENDED.

Common situations: Resuming the whole case when only some plan items were suspended; resuming an already-resumed plan item; concurrent operations changed the state before resume ran.

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