flowable/flowable-engine · error · FlowableIllegalArgumentException

deadLetterJobIds are null

Error message

deadLetterJobIds are null

What it means

Thrown by BulkMoveDeadLetterJobsCmd.execute() when the deadLetterJobIds collection is null. Flowable distinguishes null (invalid input) from empty (valid but no-op) collections; a null collection cannot back the jobIds query filter, so the command rejects it immediately.

Solutions

  1. Pass a non-null collection, even an empty one, to the bulk move API
  2. Guard the call site: if (ids != null) managementService.moveDeadLetterJobs(ids)
  3. Null-coalesce at the call: moveDeadLetterJobs(ids == null ? Collections.emptyList() : ids)

Example fix

// before
jobService.moveDeadLetterJobs(failedJobIds); // may be null
// after
if (failedJobIds != null) {
    jobService.moveDeadLetterJobs(failedJobIds);
}
Defensive patterns

Strategy: validation

Validate before calling

if (deadLetterJobIds == null) {
    throw new IllegalArgumentException("deadLetterJobIds must be non-null (use an empty list for no-op)");
}

Type guard

boolean hasIds(List<String> ids) { return ids != null; }

Try / catch

try {
    jobService.moveDeadLetterJobs(deadLetterJobIds);
} catch (FlowableIllegalArgumentException e) {
    LOGGER.error("Bulk move rejected: {}", e.getMessage());
}

Prevention

When it happens

Trigger: managementService.moveDeadLetterJobs(...) (bulk move) called with a null id list; building the id list from a stream/collection that was null; passing an optional collection without a null check.

Common situations: Collecting failed job ids from a query that returned null instead of a list; API callers omitting the ids field which binds to null; refactor merging two id sources where one is null.

Related errors


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

Appendix: source

Thrown at modules/flowable-job-service/src/main/java/org/flowable/job/service/impl/cmd/BulkMoveDeadLetterJobsCmd.java:50

public class BulkMoveDeadLetterJobsCmd implements Command<Void> {

    private static final Logger LOGGER = LoggerFactory.getLogger(BulkMoveDeadLetterJobsCmd.class);

    protected JobServiceConfiguration jobServiceConfiguration;

    protected Collection<String> deadLetterJobIds;
    protected int retries;

    public BulkMoveDeadLetterJobsCmd(Collection<String> deadLetterJobIds, int retries, JobServiceConfiguration jobServiceConfiguration) {
        this.deadLetterJobIds = deadLetterJobIds;
        this.retries = retries;
        this.jobServiceConfiguration = jobServiceConfiguration;
    }

    @Override
    public Void execute(CommandContext commandContext) {
        if (deadLetterJobIds == null) {
            throw new FlowableIllegalArgumentException("deadLetterJobIds are null");
        }

        DeadLetterJobQueryImpl query = new DeadLetterJobQueryImpl(commandContext, jobServiceConfiguration);
        query.jobIds(deadLetterJobIds);
        List<Job> deadLetterJobs = jobServiceConfiguration.getDeadLetterJobEntityManager().findJobsByQueryCriteria(query);

        for (Job job : deadLetterJobs) {
            if (HistoryJobEntity.HISTORY_JOB_TYPE.equals(job.getJobType())) {
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("Moving deadletter job to history job table {}", job.getId());
                }
                jobServiceConfiguration.getJobManager().moveDeadLetterJobToHistoryJob((DeadLetterJobEntity) job, retries);
            } else {
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("Moving deadletter job to executable job table {}", job.getId());
                }
                jobServiceConfiguration.getJobManager().moveDeadLetterJobToExecutableJob((DeadLetterJobEntity) job, retries);
            }

View on GitHub (pinned to d6d39ce1c6)