{"record":{"id":"62d85fb1c0dc4892","repo":"flowable/flowable-engine","slug":"plan-item-instance-can-only-be-resumed-if-the-stat","errorCode":null,"errorMessage":"plan item instance can only be resumed if the state is suspended","messagePattern":"plan item instance can only be resumed if the state is suspended","errorType":"validation","errorClass":"FlowableIllegalStateException","httpStatus":null,"severity":"error","filePath":"modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/agenda/operation/ResumePlanItemInstanceOperation.java","lineNumber":66,"sourceCode":"    protected void internalExecute() {\n        planItemInstanceEntity.setLastAvailableTime(getCurrentTime(commandContext));\n        \n        PlanItemDefinition planItemDefinition = planItemInstanceEntity.getPlanItem().getPlanItemDefinition();\n        if (planItemDefinition instanceof TimerEventListener) {\n            CmmnEngineConfiguration cmmnEngineConfiguration = CommandContextUtil.getCmmnEngineConfiguration(commandContext);\n            List<SuspendedJobEntity> suspendedJobs = cmmnEngineConfiguration.getJobServiceConfiguration().getSuspendedJobEntityManager().findJobsBySubScopeId(planItemInstanceEntity.getId());\n            if (suspendedJobs != null && !suspendedJobs.isEmpty()) {\n                cmmnEngineConfiguration.getJobServiceConfiguration().getJobService().activateSuspendedJob(suspendedJobs.get(0));\n            }\n        }\n        \n        CommandContextUtil.getCmmnHistoryManager(commandContext).recordPlanItemInstanceAvailable(planItemInstanceEntity);\n    }\n    \n    @Override\n    public boolean isStateNotChanged(String oldState, String newState) {\n        if (oldState != null && !PlanItemInstanceState.SUSPENDED.equals(oldState)) {\n            throw new FlowableIllegalStateException(\"plan item instance can only be resumed if the state is suspended\");\n        }\n        \n        return oldState != null && oldState.equals(newState) && abortOperationIfNewStateEqualsOldState();\n    }\n    \n    @Override\n    public boolean abortOperationIfNewStateEqualsOldState() {\n        return true;\n    }\n\n    @Override\n    public String getOperationName() {\n        return null; // Default one is ok.\n    }\n    \n}\n","sourceCodeStart":48,"sourceCodeEnd":83,"githubUrl":"https://github.com/flowable/flowable-engine/blob/d6d39ce1c69ff244f2d9dc6af756a9b95e865586/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/agenda/operation/ResumePlanItemInstanceOperation.java#L48-L83","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Check planItemInstance.getState() equals 'suspended' before resuming.","If resuming a case, note that only suspended children are affected and avoid resuming non-suspended items individually.","Serialize suspension/resume operations so concurrent agenda operations cannot change state in between.","Catch FlowableIllegalStateException and treat it as a no-op if the item is already active."],"exampleFix":"// before\ncmmnRuntimeService.resumePlanItemInstance(planItemInstanceId);\n// after\nPlanItemInstance pii = cmmnRuntimeService.createPlanItemInstanceQuery()\n    .planItemInstanceId(planItemInstanceId).singleResult();\nif (pii != null && \"suspended\".equals(pii.getState())) {\n    cmmnRuntimeService.resumePlanItemInstance(planItemInstanceId);\n}","handlingStrategy":"validation","validationCode":"// java\nPlanItemInstance pii = cmmnRuntimeService.createPlanItemInstanceQuery()\n    .planItemInstanceId(planItemInstanceId).singleResult();\nif (pii == null || !\"suspended\".equals(pii.getState()))\n    throw new IllegalStateException(\"Plan item not suspended: \" + planItemInstanceId);","typeGuard":"boolean isSuspended(PlanItemInstance pii) {\n    return pii != null && PlanItemInstanceState.SUSPENDED.equals(pii.getState());\n}","tryCatchPattern":"try {\n    cmmnRuntimeService.resumePlanItemInstance(id);\n} catch (FlowableIllegalStateException e) {\n    if (e.getMessage().contains(\"can only be resumed if the state is suspended\")) {\n        // already resumed or wrong state: treat as no-op\n    }\n}","preventionTips":["Always read current state before resume calls.","Serialize suspend/resume operations per plan item.","Handle resume of already-resumed items as idempotent no-ops in your service layer."],"tags":["cmmn","state-transition","suspend-resume"],"backgroundTag":"invalid-state-transition","analyzedSha":"d6d39ce1c69ff244f2d9dc6af756a9b95e865586","analyzedAt":"2026-09-11T06:41:19.413Z","contentChangedAt":"2026-09-11T06:41:19.413Z","schemaVersion":2},"datasetVersion":"2026-09-14T11:17:12.474Z"}