flowable/flowable-engine · error · FlowableIllegalArgumentException

Cannot move a history job to an executable job

Error message

Cannot move a history job to an executable job

What it means

moveDeadLetterJobToExecutableJob only handles regular dead-letter jobs. History jobs are explicitly excluded: attempting to move a dead-letter job whose jobType equals HistoryJobEntity.HISTORY_JOB_TYPE throws FlowableIllegalArgumentException, since history jobs are managed by separate mechanisms.

Solutions

  1. Skip jobs where HistoryJobEntity.HISTORY_JOB_TYPE.equals(job.getJobType()) in retry loops
  2. Use the history-job management APIs for history jobs instead
  3. Filter dead-letter queries to exclude history job type before bulk retry

Example fix

// before
for (DeadLetterJobEntity job : deadLetterJobs) {
    jobManager.moveDeadLetterJobToExecutableJob(job, 3);
}
// after
for (DeadLetterJobEntity job : deadLetterJobs) {
    if (!HistoryJobEntity.HISTORY_JOB_TYPE.equals(job.getJobType())) {
        jobManager.moveDeadLetterJobToExecutableJob(job, 3);
    }
}
Defensive patterns

Strategy: validation

Validate before calling

if (HistoryJobEntity.HISTORY_JOB_TYPE.equals(job.getJobType())) { log.info("Skipping history job {} in executable retry", job.getId()); return; }

Type guard

boolean isExecutableDeadLetterJob(DeadLetterJobEntity d) { return d != null && !HistoryJobEntity.HISTORY_JOB_TYPE.equals(d.getJobType()); }

Try / catch

try { jobManager.moveDeadLetterJobToExecutableJob(job, retries); } catch (FlowableIllegalArgumentException e) { log.warn("Cannot move job {} to executable: {}", job.getId(), e.getMessage()); }

Prevention

When it happens

Trigger: Calling moveDeadLetterJobToExecutableJob on a dead-letter job whose getJobType() returns "history", e.g. retrying all dead-letter jobs in a loop without filtering by type.

Common situations: Bulk retry utilities that iterate the dead-letter job table and hit history jobs that landed there via moveDeadLetterJobToHistoryJob; admin tooling assuming one uniform job type.

Understand the failure class

Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.

Related errors


AI-assisted analysis of flowable/flowable-engine@d6d39ce1c6 (2026-09-11). Data as JSON: /api/errors/23e7e8d92a77e3b0. Report an issue: GitHub.

Appendix: source

Thrown at modules/flowable-job-service/src/main/java/org/flowable/job/service/impl/asyncexecutor/DefaultJobManager.java:269

        return deadLetterJob;
    }

    protected void sendMoveToDeadletterEvent(JobInfo job) {
        FlowableEventDispatcher eventDispatcher = jobServiceConfiguration.getEventDispatcher();
        if (eventDispatcher != null && eventDispatcher.isEnabled()) {
            eventDispatcher.dispatchEvent(FlowableJobEventBuilder.createEntityEvent(
                FlowableEngineEventType.JOB_MOVED_TO_DEADLETTER, job), jobServiceConfiguration.getEngineName());
        }
    }

    @Override
    public Job moveDeadLetterJobToExecutableJob(DeadLetterJobEntity deadLetterJobEntity, int retries) {
        if (deadLetterJobEntity == null) {
            throw new FlowableIllegalArgumentException("Null job provided");
        }

        if (HistoryJobEntity.HISTORY_JOB_TYPE.equals(deadLetterJobEntity.getJobType())) {
            throw new FlowableIllegalArgumentException("Cannot move a history job to an executable job");
        }

        if (Job.JOB_TYPE_EXTERNAL_WORKER.equals(deadLetterJobEntity.getJobType())) {
            ExternalWorkerJobEntity externalWorkerJob = createExternalWorkerJobFromOtherJob(deadLetterJobEntity);
            externalWorkerJob.setRetries(retries);
            boolean insertSuccessful = jobServiceConfiguration.getExternalWorkerJobEntityManager().insertExternalWorkerJobEntity(externalWorkerJob);
            if (insertSuccessful) {
                jobServiceConfiguration.getDeadLetterJobEntityManager().delete(deadLetterJobEntity);
                return externalWorkerJob;
            }
        } else {
            JobEntity executableJob = createExecutableJobFromOtherJob(deadLetterJobEntity);
            executableJob.setRetries(retries);
            boolean insertSuccessful = jobServiceConfiguration.getJobEntityManager().insertJobEntity(executableJob);
            if (insertSuccessful) {
                jobServiceConfiguration.getDeadLetterJobEntityManager().delete(deadLetterJobEntity);
                triggerExecutorIfNeeded(executableJob);
                return executableJob;

View on GitHub (pinned to d6d39ce1c6)