flowable/flowable-engine · error · FlowableObjectNotFoundException

Could not find a dead letter job(s) with id(s)

Error message

Could not find a dead letter job(s) with id(s) {${jobIds}}

What it means

Before moving dead-letter jobs, executeDeadLetterJobAction counts how many of the supplied jobIds actually exist. If the count does not match the request size, it lists the found jobs, computes the missing ids, and throws FlowableObjectNotFoundException naming the ids that could not be found.

Solutions

  1. Re-fetch the dead-letter job list (GET /management/deadletter-jobs) and submit only ids from it.
  2. Remove the ids named in the error message and retry the bulk action with the remaining ones.
  3. Check whether the missing ids belong to another job table and use the matching endpoint.
  4. Serialize bulk operations so two operators cannot move the same jobs concurrently.

Example fix

// before
{"action":"move","jobIds":["10","11","12"]}   // 12 already moved -> 404
// after: query first
curl http://localhost:8080/flowable-rest/management/deadletter-jobs
# submit only ids still present
{"action":"move","jobIds":["10","11"]}
Defensive patterns

Strategy: validation

Validate before calling

const deadletter = await fetch(`${base}/management/deadletter-jobs?size=1000`).then(r => r.json());
const existing = new Set(deadletter.data.map(j => j.id));
const missing = jobIds.filter(id => !existing.has(id));
if (missing.length) throw new Error(`Not dead-letter jobs: ${missing.join(',')}`);

Try / catch

try {
  await bulkMoveDeadLetterJobs({action:'move', jobIds});
} catch (e) {
  if (e.status === 404) {
    const missing = parseMissingIds(e.message); // ids listed in the message
    const remaining = jobIds.filter(id => !missing.includes(id));
    if (remaining.length) return bulkMoveDeadLetterJobs({action:'move', jobIds: remaining});
  }
  throw e;
}

Prevention

When it happens

Trigger: POST /management/deadletter-jobs with a jobIds list containing one or more ids that are not dead-letter jobs (already moved, completed, or never existed).

Common situations: Bulk retry scripts built from stale lists, another operator already moved the jobs, or ids taken from timer/suspended/history job tables instead of dead-letter jobs.

Understand the failure class

Background: "Not found" and "does not exist" errors: why "Task not found", "No such folder", and "Can't find" fire when a lookup comes back empty — this error's family across 14 libraries.

Related errors


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

Appendix: source

Thrown at modules/flowable-rest/src/main/java/org/flowable/rest/service/api/management/JobCollectionResource.java:211

    @ApiResponses(value = {
            @ApiResponse(code = 204, message = "Indicates the dead letter jobs where moved. Response-body is intentionally empty."),
            @ApiResponse(code = 500, message = "Indicates the an exception occurred while executing the job. The status-description contains additional detail about the error. The full error-stacktrace can be fetched later on if needed.")
    })
    @PostMapping("/management/deadletter-jobs")
    @ResponseStatus(value = HttpStatus.NO_CONTENT)
    public void executeDeadLetterJobAction(@RequestBody BulkMoveDeadLetterActionRequest actionRequest) {
        if (actionRequest == null || !(MOVE_ACTION.equals(actionRequest.getAction()) || MOVE_TO_HISTORY_JOB_ACTION.equals(actionRequest.getAction()))) {
            throw new FlowableIllegalArgumentException("Invalid action, only 'move' or 'moveToHistoryJob' is supported.");
        }

        List<String> jobIds = actionRequest.getJobIds();
        long existingJobIdCount = managementService.createDeadLetterJobQuery().jobIds(jobIds).count();
        if (jobIds.size() != existingJobIdCount) {
            List<Job> foundJobs = managementService.createDeadLetterJobQuery().jobIds(jobIds).list();
            for (Job job : foundJobs) {
                jobIds.remove(job.getId());
            }
            throw new FlowableObjectNotFoundException(
                    "Could not find a dead letter job(s) with id(s) {" + jobIds.stream().collect(Collectors.joining(",")) + "}", Job.class);
        }
        if (MOVE_ACTION.equals(actionRequest.getAction())) {
            if (restApiInterceptor != null) {
                restApiInterceptor.bulkMoveDeadLetterJobs(actionRequest.getJobIds(), MOVE_ACTION);
            }
            managementService.bulkMoveDeadLetterJobs(jobIds, processEngineConfiguration.getAsyncExecutorNumberOfRetries());
        } else if (MOVE_TO_HISTORY_JOB_ACTION.equals(actionRequest.getAction())) {
            if (restApiInterceptor != null) {
                restApiInterceptor.bulkMoveDeadLetterJobs(actionRequest.getJobIds(), MOVE_TO_HISTORY_JOB_ACTION);
            }
            managementService.bulkMoveDeadLetterJobsToHistoryJobs(jobIds, processEngineConfiguration.getAsyncHistoryExecutorNumberOfRetries());
        }
    }
}

View on GitHub (pinned to d6d39ce1c6)