{"record":{"id":"a063119c67a20375","repo":"flowable/flowable-engine","slug":"plan-item-instance-is-already-suspended","errorCode":null,"errorMessage":"plan item instance is already suspended","messagePattern":"plan item instance is already suspended","errorType":"exception","errorClass":"FlowableIllegalStateException","httpStatus":null,"severity":"error","filePath":"modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/agenda/operation/SuspendPlanItemInstanceOperation.java","lineNumber":64,"sourceCode":"        planItemInstanceEntity.setLastSuspendedTime(getCurrentTime(commandContext));\n        \n        PlanItemDefinition planItemDefinition = planItemInstanceEntity.getPlanItem().getPlanItemDefinition();\n        if (planItemDefinition instanceof TimerEventListener) {\n            CmmnEngineConfiguration cmmnEngineConfiguration = CommandContextUtil.getCmmnEngineConfiguration(commandContext);\n            List<TimerJobEntity> timerJobs = cmmnEngineConfiguration.getJobServiceConfiguration().getTimerJobEntityManager().findJobsByScopeIdAndSubScopeId(\n                    planItemInstanceEntity.getCaseInstanceId(), planItemInstanceEntity.getId());\n            if (timerJobs != null && !timerJobs.isEmpty()) {\n                cmmnEngineConfiguration.getJobServiceConfiguration().getJobService().moveJobToSuspendedJob(timerJobs.get(0));\n            }\n        }\n        \n        CommandContextUtil.getCmmnHistoryManager(commandContext).recordPlanItemInstanceSuspended(planItemInstanceEntity);\n    }\n    \n    @Override\n    public boolean isStateNotChanged(String oldState, String newState) {\n        if (oldState != null && oldState.equals(newState)) {\n            throw new FlowableIllegalStateException(\"plan item instance is already suspended\");\n        }\n        \n        return false;\n    }\n\n    @Override\n    public boolean abortOperationIfNewStateEqualsOldState() {\n        return true;\n    }\n\n    @Override\n    public String getOperationName() {\n        return \"[Suspend plan item]\";\n    }\n}\n","sourceCodeStart":46,"sourceCodeEnd":80,"githubUrl":"https://github.com/flowable/flowable-engine/blob/d6d39ce1c69ff244f2d9dc6af756a9b95e865586/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/agenda/operation/SuspendPlanItemInstanceOperation.java#L46-L80","documentation":"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.","triggerScenarios":"Calling suspendPlanItemInstance (or suspending a case whose item is already suspended) for a plan item instance whose state is already 'suspended'.","commonSituations":"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.","solutions":["Query the plan item instance and skip suspension when state is already 'suspended'.","Make suspend actions idempotent at the application layer (guard with state check or lock).","Catch FlowableIllegalStateException and treat it as success/no-op when appropriate.","Avoid suspending a container whose children are partially suspended without checking states."],"exampleFix":"// before\ncmmnRuntimeService.suspendPlanItemInstance(planItemInstanceId);\n// after\nPlanItemInstance pii = cmmnRuntimeService.createPlanItemInstanceQuery()\n    .planItemInstanceId(planItemInstanceId).singleResult();\nif (pii != null && !\"suspended\".equals(pii.getState())) {\n    cmmnRuntimeService.suspendPlanItemInstance(planItemInstanceId);\n}","handlingStrategy":"validation","validationCode":"// java\nPlanItemInstance pii = cmmnRuntimeService.createPlanItemInstanceQuery()\n    .planItemInstanceId(planItemInstanceId).singleResult();\nif (pii == null || \"suspended\".equals(pii.getState())) return; // nothing to do","typeGuard":"boolean needsSuspend(PlanItemInstance pii) {\n    return pii != null && !PlanItemInstanceState.SUSPENDED.equals(pii.getState());\n}","tryCatchPattern":"try {\n    cmmnRuntimeService.suspendPlanItemInstance(id);\n} catch (FlowableIllegalStateException e) {\n    if (e.getMessage().contains(\"already suspended\")) {\n        // idempotent no-op\n    }\n}","preventionTips":["Guard suspend UI actions against double submission.","Check child states before suspending partially suspended containers.","Wrap suspend APIs in idempotent service methods."],"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"}