flowable/flowable-engine · error · FlowableIllegalArgumentException
Invalid action, only 'move' or 'moveToHistoryJob' is…
Error message
Invalid action, only 'move' or 'moveToHistoryJob' is supported.
What it means
FlowableIllegalArgumentException thrown by POST /management/deadletter-jobs/{jobId} when the action is not 'move' or 'moveToHistoryJob'. The deadletter-job endpoint accepts a different action set than the timer-jobs endpoint, and the request body or action value failing this check is rejected before any job is loaded.
Solutions
- Use {"action":"move"} to move the deadletter job back to the executable job table
- Use {"action":"moveToHistoryJob"} to move it to the history (timer) job table
- Remove 'reschedule' — it is only valid on /management/timer-jobs, not deadletter jobs
- Check action spelling/casing against the constants MOVE_ACTION and MOVE_TO_HISTORY_JOB_ACTION
Example fix
// before
POST /management/deadletter-jobs/42 {"action":"reschedule","dueDate":"..."}
// after
POST /management/deadletter-jobs/42 {"action":"move"} Defensive patterns
Strategy: validation
Validate before calling
const DEADLETTER_ACTIONS = ['move', 'moveToHistoryJob'];
if (!body || !DEADLETTER_ACTIONS.includes(body.action)) throw new Error("deadletter action must be 'move' or 'moveToHistoryJob'"); Type guard
function isDeadLetterJobAction(a) { return a === 'move' || a === 'moveToHistoryJob'; } Try / catch
try { await post(`/management/deadletter-jobs/${id}`, body); } catch (e) { if (e.status === 400) fixDeadLetterPayload(e); else throw e; } Prevention
- Never reuse timer-jobs action payloads (reschedule) for deadletter jobs
- Validate the action against the deadletter-specific allowed set
- Send a JSON body with Content-Type: application/json on every action call
When it happens
Trigger: POST /management/deadletter-jobs/{jobId} with (a) missing/empty body, (b) no 'action' field, or (c) action other than 'move' or 'moveToHistoryJob' — e.g. reusing 'reschedule' from the timer-jobs endpoint, which is not valid for deadletter jobs.
Common situations: Developers reusing the timer-job client code against deadletter jobs (sending 'reschedule'), typos like 'movetohistoryjob', missing JSON body, or copying examples for an older Flowable version with a different action vocabulary.
Understand the failure class
Background: Invalid enum value errors: "Unknown type", "Invalid scope", "must be one of" — when a string is not on the library's allowed list — this error's family across 23 libraries.
Related errors
- A request body was expected when executing the form submit.
- Attachment name is required.
- Error converting request body to RestVariable instance
- Id cannot be null.
- Illegal action: ' '.
AI-assisted analysis of flowable/flowable-engine@d6d39ce1c6 (2026-09-11).
Data as JSON: /api/errors/1d44613296921cd2.
Report an issue: GitHub.
Appendix: source
Thrown at modules/flowable-rest/src/main/java/org/flowable/rest/service/api/management/JobResource.java:319
managementService.rescheduleTimeDateJob(job.getId(), actionRequest.getDueDate());
} catch (FlowableObjectNotFoundException e) {
// Re-throw to have consistent error-messaging across REST-api
throw new FlowableObjectNotFoundException("Could not find a timer job with id '" + jobId + "'.", Job.class);
}
}
}
@ApiOperation(value = "Move a single deadletter job. Accepts 'move' and 'moveToHistoryJob' as action.", tags = { "Jobs" }, code = 204)
@ApiResponses(value = {
@ApiResponse(code = 204, message = "Indicates the dead letter job was moved. Response-body is intentionally empty."),
@ApiResponse(code = 404, message = "Indicates the requested job was not found."),
@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/{jobId}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void executeDeadLetterJobAction(@ApiParam(name = "jobId") @PathVariable String jobId, @RequestBody RestActionRequest 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.");
}
Job deadLetterJob = getDeadLetterJobById(jobId);
if (MOVE_ACTION.equals(actionRequest.getAction())) {
/*
* Note that the jobType is checked to know which kind of move that needs to be done.
* The MOVE_TO_HISTORY_JOB_ACTION allows to specifically force the move to a history job and trigger the else part below.
*/
try {
if (HistoryJobEntity.HISTORY_JOB_TYPE.equals(deadLetterJob.getJobType())) {
managementService.moveDeadLetterJobToHistoryJob(deadLetterJob.getId(), processEngineConfiguration.getAsyncExecutorNumberOfRetries());
} else {
managementService.moveDeadLetterJobToExecutableJob(deadLetterJob.getId(), processEngineConfiguration.getAsyncExecutorNumberOfRetries());
View on GitHub (pinned to d6d39ce1c6)