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

  1. Check HistoryJobEntity.HISTORY_JOB_TYPE.equals(job.getJobType()) before the call
  2. Route non-history dead-letter jobs to moveDeadLetterJobToExecutableJob instead
  3. 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

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


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)