{"record":{"id":"50bcee9aa8bf3173","repo":"flowable/flowable-engine","slug":"can-only-move-a-history-job-to-a-history-job","errorCode":null,"errorMessage":"Can only move a history job to a history job","messagePattern":"Can only move a history job to a history job","errorType":"validation","errorClass":"FlowableIllegalArgumentException","httpStatus":null,"severity":"error","filePath":"modules/flowable-job-service/src/main/java/org/flowable/job/service/impl/asyncexecutor/DefaultJobManager.java","lineNumber":301,"sourceCode":"            boolean insertSuccessful = jobServiceConfiguration.getJobEntityManager().insertJobEntity(executableJob);\n            if (insertSuccessful) {\n                jobServiceConfiguration.getDeadLetterJobEntityManager().delete(deadLetterJobEntity);\n                triggerExecutorIfNeeded(executableJob);\n                return executableJob;\n            }\n        }\n\n        return null;\n    }\n\n    @Override\n    public HistoryJobEntity moveDeadLetterJobToHistoryJob(DeadLetterJobEntity deadLetterJobEntity, int retries) {\n        if (deadLetterJobEntity == null) {\n            throw new FlowableIllegalArgumentException(\"Null job provided\");\n        }\n\n        if (!HistoryJobEntity.HISTORY_JOB_TYPE.equals(deadLetterJobEntity.getJobType())) {\n            throw new FlowableIllegalArgumentException(\"Can only move a history job to a history job\");\n        }\n\n        HistoryJobEntityManager historyJobEntityManager = jobServiceConfiguration.getHistoryJobEntityManager();\n        HistoryJobEntity historyJobEntity = historyJobEntityManager.create();\n        copyHistoryJobProperties(historyJobEntity, deadLetterJobEntity);\n\n        historyJobEntity.setRetries(retries);\n        historyJobEntity.setJobHandlerConfiguration(null); // special case: the deadletter jobConfiguration had the history json bytearray as reference in the configuration\n\n        // Need to copy the bytes, because the delete of the deadLetterJobEntity will delete the byte array too\n        // (which is needed when the deadLetterJob gets removed through the API service, so the byte array deletion can't be removed from there)\n        ByteArrayEntity byteArrayEntity = getCommandContext().getEngineConfigurations().get(jobServiceConfiguration.getEngineName())\n            .getByteArrayEntityManager().findById(deadLetterJobEntity.getJobHandlerConfiguration());\n        historyJobEntity.setAdvancedJobHandlerConfigurationBytes(byteArrayEntity.getBytes());\n\n        historyJobEntityManager.insert(historyJobEntity);\n        jobServiceConfiguration.getDeadLetterJobEntityManager().delete(deadLetterJobEntity);\n","sourceCodeStart":283,"sourceCodeEnd":319,"githubUrl":"https://github.com/flowable/flowable-engine/blob/d6d39ce1c69ff244f2d9dc6af756a9b95e865586/modules/flowable-job-service/src/main/java/org/flowable/job/service/impl/asyncexecutor/DefaultJobManager.java#L283-L319","documentation":"moveDeadLetterJobToHistoryJob is only valid for dead-letter jobs whose type is HistoryJobEntity.HISTORY_JOB_TYPE. If getJobType() is anything else, the method throws FlowableIllegalArgumentException because moving a non-history job to the history job table would corrupt job semantics.","triggerScenarios":"Calling moveDeadLetterJobToHistoryJob on a dead-letter async/timer/external-worker job (getJobType() != \"history\"), commonly in a loop over all dead-letter jobs without type filtering.","commonSituations":"Bulk history-cleanup scripts that iterate the dead-letter table; retry tooling confusing moveDeadLetterJobToHistoryJob with moveDeadLetterJobToExecutableJob.","solutions":["Check HistoryJobEntity.HISTORY_JOB_TYPE.equals(job.getJobType()) before the call","Route non-history dead-letter jobs to moveDeadLetterJobToExecutableJob instead","Separate bulk handlers per job type when processing dead-letter jobs"],"exampleFix":"// before\njobManager.moveDeadLetterJobToHistoryJob(job, 3); // job is an async job\n// after\nif (HistoryJobEntity.HISTORY_JOB_TYPE.equals(job.getJobType())) {\n    jobManager.moveDeadLetterJobToHistoryJob(job, 3);\n} else {\n    jobManager.moveDeadLetterJobToExecutableJob(job, 3);\n}","handlingStrategy":"validation","validationCode":"if (!HistoryJobEntity.HISTORY_JOB_TYPE.equals(job.getJobType())) { throw new IllegalArgumentException(\"Job \" + job.getId() + \" is not a history job\"); }","typeGuard":"boolean isHistoryDeadLetterJob(DeadLetterJobEntity d) { return d != null && HistoryJobEntity.HISTORY_JOB_TYPE.equals(d.getJobType()); }","tryCatchPattern":"try { jobManager.moveDeadLetterJobToHistoryJob(job, retries); } catch (FlowableIllegalArgumentException e) { log.warn(\"Job {} is not a history job: {}\", job.getId(), e.getMessage()); }","preventionTips":["Check jobType before routing dead-letter jobs","Use moveDeadLetterJobToExecutableJob for non-history jobs","Split bulk handlers per job type"],"tags":["flowable","job-scheduling","history-job","type-mismatch"],"backgroundTag":"invalid-argument-value","analyzedSha":"d6d39ce1c69ff244f2d9dc6af756a9b95e865586","analyzedAt":"2026-09-11T06:41:19.413Z","contentChangedAt":"2026-09-11T06:41:19.413Z","schemaVersion":2},"datasetVersion":"2026-09-18T11:17:12.947Z"}