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

  1. Use {"action":"move"} to move the deadletter job back to the executable job table
  2. Use {"action":"moveToHistoryJob"} to move it to the history (timer) job table
  3. Remove 'reschedule' — it is only valid on /management/timer-jobs, not deadletter jobs
  4. 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

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


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)