flowable/flowable-engine · error · FlowableIllegalArgumentException
Can only move a history job to a history job
Error message
Can only move a history job to a history job
What it means
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.
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
Example fix
// before
jobManager.moveDeadLetterJobToHistoryJob(job, 3); // job is an async job
// after
if (HistoryJobEntity.HISTORY_JOB_TYPE.equals(job.getJobType())) {
jobManager.moveDeadLetterJobToHistoryJob(job, 3);
} else {
jobManager.moveDeadLetterJobToExecutableJob(job, 3);
} Defensive patterns
Strategy: validation
Validate before calling
if (!HistoryJobEntity.HISTORY_JOB_TYPE.equals(job.getJobType())) { throw new IllegalArgumentException("Job " + job.getId() + " is not a history job"); } Type guard
boolean isHistoryDeadLetterJob(DeadLetterJobEntity d) { return d != null && HistoryJobEntity.HISTORY_JOB_TYPE.equals(d.getJobType()); } Try / catch
try { jobManager.moveDeadLetterJobToHistoryJob(job, retries); } catch (FlowableIllegalArgumentException e) { log.warn("Job {} is not a history job: {}", job.getId(), e.getMessage()); } Prevention
- Check jobType before routing dead-letter jobs
- Use moveDeadLetterJobToExecutableJob for non-history jobs
- Split bulk handlers per job type
When it happens
Trigger: 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.
Common situations: Bulk history-cleanup scripts that iterate the dead-letter table; retry tooling confusing moveDeadLetterJobToHistoryJob with moveDeadLetterJobToExecutableJob.
Understand the failure class
Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.
Related errors
- Cannot move a history job to an executable job
- Cannot move the job to deadletter: the job is not a timer…
- App resource is not of type AppModel
- Can only use a collection of String elements for…
- Cannot convert event payload
AI-assisted analysis of flowable/flowable-engine@d6d39ce1c6 (2026-09-11).
Data as JSON: /api/errors/50bcee9aa8bf3173.
Report an issue: GitHub.
Appendix: source
Thrown at modules/flowable-job-service/src/main/java/org/flowable/job/service/impl/asyncexecutor/DefaultJobManager.java:301
boolean insertSuccessful = jobServiceConfiguration.getJobEntityManager().insertJobEntity(executableJob);
if (insertSuccessful) {
jobServiceConfiguration.getDeadLetterJobEntityManager().delete(deadLetterJobEntity);
triggerExecutorIfNeeded(executableJob);
return executableJob;
}
}
return null;
}
@Override
public HistoryJobEntity moveDeadLetterJobToHistoryJob(DeadLetterJobEntity deadLetterJobEntity, int retries) {
if (deadLetterJobEntity == null) {
throw new FlowableIllegalArgumentException("Null job provided");
}
if (!HistoryJobEntity.HISTORY_JOB_TYPE.equals(deadLetterJobEntity.getJobType())) {
throw new FlowableIllegalArgumentException("Can only move a history job to a history job");
}
HistoryJobEntityManager historyJobEntityManager = jobServiceConfiguration.getHistoryJobEntityManager();
HistoryJobEntity historyJobEntity = historyJobEntityManager.create();
copyHistoryJobProperties(historyJobEntity, deadLetterJobEntity);
historyJobEntity.setRetries(retries);
historyJobEntity.setJobHandlerConfiguration(null); // special case: the deadletter jobConfiguration had the history json bytearray as reference in the configuration
// Need to copy the bytes, because the delete of the deadLetterJobEntity will delete the byte array too
// (which is needed when the deadLetterJob gets removed through the API service, so the byte array deletion can't be removed from there)
ByteArrayEntity byteArrayEntity = getCommandContext().getEngineConfigurations().get(jobServiceConfiguration.getEngineName())
.getByteArrayEntityManager().findById(deadLetterJobEntity.getJobHandlerConfiguration());
historyJobEntity.setAdvancedJobHandlerConfigurationBytes(byteArrayEntity.getBytes());
historyJobEntityManager.insert(historyJobEntity);
jobServiceConfiguration.getDeadLetterJobEntityManager().delete(deadLetterJobEntity);
View on GitHub (pinned to d6d39ce1c6)