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
- Re-fetch the dead-letter job list (GET /management/deadletter-jobs) and submit only ids from it.
- Remove the ids named in the error message and retry the bulk action with the remaining ones.
- Check whether the missing ids belong to another job table and use the matching endpoint.
- 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
- Always source jobIds from a live deadletter-jobs query, never cached lists.
- Retry with the ids named in the 404 message removed.
- Coordinate bulk moves so concurrent operators do not move the same jobs.
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
- ${aonfe.getMessage()}
- Batch part with id ' ' does not have a batch part document.
- Batch with id ' ' does not have a batch document.
- Case definition does not have a start form defined
- Case definition with id
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)