kestra-io/kestra · error · IllegalArgumentException
You can only change the state of a task run for a terminated
Error message
You can only change the state of a task run for a terminated non killed execution.
What it means
ExecutionService.changeTaskRunState only allows changing a task run's state when the containing execution is in a terminal state that is not KILLED. The guard is State.canChangeStatus(), defined as isTerminated() && !isKilled() (State.java:189-190). Terminated types include FAILED, WARNING, SUCCESS, CANCELLED, RETRIED, SKIPPED, RESUBMITTED — KILLED is explicitly excluded because a killed execution must not be revived by editing a single task run. The check runs AFTER markAs already mutated state, so the exception discards that in-memory work.
Source
Thrown at core/src/main/java/io/kestra/core/services/ExecutionService.java:554
if (targetExecution.getState().canChangeStatus()) {
List<TaskRun> newTaskRuns = newExecution.getTaskRunList();
// We need to remove global error tasks and flowable error tasks if any
flow
.allErrorsWithChildren()
.forEach(task -> newTaskRuns.removeIf(taskRun -> taskRun.getTaskId().equals(task.getId())));
// We need to remove global finally tasks and flowable error tasks if any
flow
.allFinallyWithChildren()
.forEach(task -> newTaskRuns.removeIf(taskRun -> taskRun.getTaskId().equals(task.getId())));
// We need to remove afterExecution tasks
ListUtils.emptyOnNull(flow.getAfterExecution())
.forEach(task -> newTaskRuns.removeIf(taskRun -> taskRun.getTaskId().equals(task.getId())));
newExecution = newExecution.withTaskRunList(newTaskRuns);
} else {
throw new IllegalArgumentException("You can only change the state of a task run for a terminated non killed execution.");
}
eventPublisher.publishEvent(CrudEvent.of(targetExecution, newExecution));
return newExecution;
}
/**
* Find the execution (main or loop sub-execution) that contains the given task run.
* Searches the given execution first; if not found, searches loop sub-executions.
*
* @param execution the parent execution to search first
* @param taskRunId the task run ID to find
* @return the execution and task run pair, or empty if not found in any execution
*/
public Optional<ExecutionWithTaskRun> findExecutionWithTaskRun(Execution execution, String taskRunId) {
Optional<TaskRun> maybeTaskRun = ListUtils.emptyOnNull(execution.getTaskRunList()).stream()
.filter(tr -> tr.getId().equals(taskRunId))
.findFirst();View on GitHub (pinned to 823fada927)
Solutions
- Before calling changeTaskRunState, fetch the latest execution and assert execution.getState().canChangeStatus() (terminated and not KILLED). Reject or wait-and-retry if false.
- If the execution is KILLED, do not use changeTaskRunState — restart or replay the execution via ExecutionService.restart or the restart API instead.
- If the execution is still running, wait for it to reach a terminal state (poll execution state, or subscribe to the execution queue) before issuing the change.
- Guard the UI/API call site with a 409 Conflict response when canChangeStatus() is false, so clients get a clear signal instead of a 500 from the IllegalArgumentException.
Example fix
// before
executionService.changeTaskRunState(execution, flow, taskRunId, State.Type.SUCCESS);
// after
if (!execution.getState().canChangeStatus()) {
throw new IllegalStateException(
"Cannot change task run state: execution is %s.".formatted(execution.getState().getCurrent())
);
}
executionService.changeTaskRunState(execution, flow, taskRunId, State.Type.SUCCESS); Defensive patterns
Strategy: validation
Validate before calling
import io.kestra.core.models.executions.Execution;
Execution fresh = executionRepository.findById(tenantId, executionId).orElseThrow();
if (!fresh.getState().canChangeStatus()) {
throw new IllegalStateException(
"Execution %s is in state %s; changeTaskRunState requires a terminated, non-killed execution.".formatted(
fresh.getId(), fresh.getState().getCurrent())
);
}
executionService.changeTaskRunState(fresh, flow, taskRunId, newState); Type guard
// Java guard helper — gate any changeTaskRunState call through this.
public static boolean canChangeTaskRunState(Execution execution) {
return execution != null && execution.getState().canChangeStatus();
} Try / catch
try {
executionService.changeTaskRunState(execution, flow, taskRunId, newState);
} catch (IllegalArgumentException e) {
// re-fetch, surface a 409 Conflict with the current execution state,
// and do NOT retry unchanged — the state machine must be reconciled first.
throw new ConflictException("Execution %s cannot have its task run state changed: %s".formatted(
execution.getId(), e.getMessage()));
} Prevention
- Fetch the execution immediately before mutating it; do not trust a stale execution object from a prior screen/API call.
- Centralize task-run state changes behind one service method that asserts canChangeStatus() first, so no call site can bypass the guard.
- Treat KILLED executions as non-editable: route them through restart/replay instead of changeTaskRunState.
- Return HTTP 409 (not 500) at the controller boundary when the guard fails, so clients can distinguish a state conflict from a server fault.
When it happens
Trigger: Calling the change-task-run-state API (or ExecutionService.changeTaskRunState) on an execution whose current State.Type is KILLED, or on a non-terminated execution such as RUNNING, CREATED, PAUSED, QUEUED, RETRYING, or BREAKPOINT. Common entry points: the UI 'change state' action on a task run, a REST call to the execution controller, or a programmatic call while the execution is still being processed by the executor/worker.
Common situations: Editing a task run on an execution that is still RUNNING (worker hasn't finished); editing after the execution was killed via the kill API; a race where the execution transitions between the time the UI fetched it and the time the change request is sent; retrying automation that does not re-check execution state before issuing the change.
Related errors
- The execution is not paused
- Only QUEUED execution can be unqueued
- Invalid target state: {state}. Valid states are: {VALID_TARG
- Execution '{executionId}' is not paused, can't resume it
- Execution must be terminated or paused and not killed to be
AI-assisted analysis of kestra-io/kestra@823fada927 (2026-08-14).
Data as JSON: /api/errors/409dd0560b2819e7.
Report an issue: GitHub.