{"record":{"id":"050824e9a11d375e","repo":"flowable/flowable-engine","slug":"could-not-find-a-dead-letter-job-s-with-id-s-050824","errorCode":null,"errorMessage":"Could not find a dead letter job(s) with id(s) {${jobIds}}","messagePattern":"Could not find a dead letter job\\(s\\) with id\\(s\\) (.+?)","errorType":"http","errorClass":"FlowableObjectNotFoundException","httpStatus":404,"severity":"error","filePath":"modules/flowable-rest/src/main/java/org/flowable/rest/service/api/management/JobCollectionResource.java","lineNumber":211,"sourceCode":"    @ApiResponses(value = {\n            @ApiResponse(code = 204, message = \"Indicates the dead letter jobs where moved. Response-body is intentionally empty.\"),\n            @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.\")\n    })\n    @PostMapping(\"/management/deadletter-jobs\")\n    @ResponseStatus(value = HttpStatus.NO_CONTENT)\n    public void executeDeadLetterJobAction(@RequestBody BulkMoveDeadLetterActionRequest actionRequest) {\n        if (actionRequest == null || !(MOVE_ACTION.equals(actionRequest.getAction()) || MOVE_TO_HISTORY_JOB_ACTION.equals(actionRequest.getAction()))) {\n            throw new FlowableIllegalArgumentException(\"Invalid action, only 'move' or 'moveToHistoryJob' is supported.\");\n        }\n\n        List<String> jobIds = actionRequest.getJobIds();\n        long existingJobIdCount = managementService.createDeadLetterJobQuery().jobIds(jobIds).count();\n        if (jobIds.size() != existingJobIdCount) {\n            List<Job> foundJobs = managementService.createDeadLetterJobQuery().jobIds(jobIds).list();\n            for (Job job : foundJobs) {\n                jobIds.remove(job.getId());\n            }\n            throw new FlowableObjectNotFoundException(\n                    \"Could not find a dead letter job(s) with id(s) {\" + jobIds.stream().collect(Collectors.joining(\",\")) + \"}\", Job.class);\n        }\n        if (MOVE_ACTION.equals(actionRequest.getAction())) {\n            if (restApiInterceptor != null) {\n                restApiInterceptor.bulkMoveDeadLetterJobs(actionRequest.getJobIds(), MOVE_ACTION);\n            }\n            managementService.bulkMoveDeadLetterJobs(jobIds, processEngineConfiguration.getAsyncExecutorNumberOfRetries());\n        } else if (MOVE_TO_HISTORY_JOB_ACTION.equals(actionRequest.getAction())) {\n            if (restApiInterceptor != null) {\n                restApiInterceptor.bulkMoveDeadLetterJobs(actionRequest.getJobIds(), MOVE_TO_HISTORY_JOB_ACTION);\n            }\n            managementService.bulkMoveDeadLetterJobsToHistoryJobs(jobIds, processEngineConfiguration.getAsyncHistoryExecutorNumberOfRetries());\n        }\n    }\n}\n","sourceCodeStart":193,"sourceCodeEnd":227,"githubUrl":"https://github.com/flowable/flowable-engine/blob/d6d39ce1c69ff244f2d9dc6af756a9b95e865586/modules/flowable-rest/src/main/java/org/flowable/rest/service/api/management/JobCollectionResource.java#L193-L227","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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."],"exampleFix":"// before\n{\"action\":\"move\",\"jobIds\":[\"10\",\"11\",\"12\"]}   // 12 already moved -> 404\n// after: query first\ncurl http://localhost:8080/flowable-rest/management/deadletter-jobs\n# submit only ids still present\n{\"action\":\"move\",\"jobIds\":[\"10\",\"11\"]}","handlingStrategy":"validation","validationCode":"const deadletter = await fetch(`${base}/management/deadletter-jobs?size=1000`).then(r => r.json());\nconst existing = new Set(deadletter.data.map(j => j.id));\nconst missing = jobIds.filter(id => !existing.has(id));\nif (missing.length) throw new Error(`Not dead-letter jobs: ${missing.join(',')}`);","typeGuard":null,"tryCatchPattern":"try {\n  await bulkMoveDeadLetterJobs({action:'move', jobIds});\n} catch (e) {\n  if (e.status === 404) {\n    const missing = parseMissingIds(e.message); // ids listed in the message\n    const remaining = jobIds.filter(id => !missing.includes(id));\n    if (remaining.length) return bulkMoveDeadLetterJobs({action:'move', jobIds: remaining});\n  }\n  throw e;\n}","preventionTips":["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."],"tags":["rest-api","deadletter-jobs","not-found","bulk-operation"],"backgroundTag":"entity-not-found","analyzedSha":"d6d39ce1c69ff244f2d9dc6af756a9b95e865586","analyzedAt":"2026-09-11T06:41:19.413Z","contentChangedAt":"2026-09-11T06:41:19.413Z","schemaVersion":2},"datasetVersion":"2026-09-18T11:17:12.947Z"}