flowable/flowable-engine · error · FlowableIllegalArgumentException

Cannot move the job to deadletter: the job is not a timer…

Error message

Cannot move the job to deadletter: the job is not a timer, async job or external worker job

What it means

moveJobToDeadLetterJob moves a timer, async, or external-worker job to the dead-letter table after repeated failures. If the job object is none of the recognized types (TimerJobEntity, JobEntity, ExternalWorkerJobEntity, or their dead-letter variants handled earlier), the library throws FlowableIllegalArgumentException because dead-lettering that job type is unsupported.

Solutions

  1. Filter by job type before calling: only pass timer, async (JobEntity), or external-worker jobs
  2. Handle history jobs with moveDeadLetterJobToHistoryJob or dedicated history-job APIs instead
  3. Update custom failure handlers when new Flowable job types are introduced

Example fix

// before
jobManager.moveJobToDeadLetterJob(job); // job may be a history job
// after
if (job instanceof TimerJobEntity || job instanceof JobEntity || job instanceof ExternalWorkerJobEntity) {
    jobManager.moveJobToDeadLetterJob(job);
} else {
    // route other job types appropriately
}
Defensive patterns

Strategy: type-guard

Validate before calling

if (!(job instanceof TimerJobEntity) && !(job instanceof JobEntity) && !(job instanceof ExternalWorkerJobEntity)) { throw new IllegalArgumentException("Unsupported job type for deadletter: " + job.getClass()); }

Type guard

boolean canMoveToDeadLetter(JobInfo job) { return job instanceof TimerJobEntity || job instanceof JobEntity || job instanceof ExternalWorkerJobEntity; }

Try / catch

try { jobManager.moveJobToDeadLetterJob(job); } catch (FlowableIllegalArgumentException e) { log.error("Job type not supported for deadletter: {}", job.getClass(), e); }

Prevention

When it happens

Trigger: Calling moveJobToDeadLetterJob with a HistoryJobEntity, suspended-job wrapper, or other JobInfo subtype not handled by the instanceof chain; passing a custom Job implementation.

Common situations: Custom failure-handling code that forwards every failed job to the dead-letter path without filtering by type; framework upgrades adding new job types that old handler code funnels into moveJobToDeadLetterJob.

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/09ca07b2061f67bd. Report an issue: GitHub.

Appendix: source

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

        }

        jobServiceConfiguration.getSuspendedJobEntityManager().delete(job);
        return activatedJob;
    }

    @Override
    public DeadLetterJobEntity moveJobToDeadLetterJob(AbstractRuntimeJobEntity job) {
        DeadLetterJobEntity deadLetterJob = createDeadLetterJobFromOtherJob(job);
        jobServiceConfiguration.getDeadLetterJobEntityManager().insert(deadLetterJob);
        if (job instanceof TimerJobEntity) {
            jobServiceConfiguration.getTimerJobEntityManager().delete((TimerJobEntity) job);

        } else if (job instanceof JobEntity) {
            jobServiceConfiguration.getJobEntityManager().delete((JobEntity) job);
        } else if (job instanceof ExternalWorkerJobEntity) {
            jobServiceConfiguration.getExternalWorkerJobEntityManager().delete((ExternalWorkerJobEntity) job);
        } else {
            throw new FlowableIllegalArgumentException("Cannot move the job to deadletter: the job is not a timer, async job or external worker job");
        }

        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");
        }

View on GitHub (pinned to d6d39ce1c6)