{"record":{"id":"409dd0560b2819e7","repo":"kestra-io/kestra","slug":"you-can-only-change-the-state-of-a-task-run-for-a","errorCode":null,"errorMessage":"You can only change the state of a task run for a terminated non killed execution.","messagePattern":"You can only change the state of a task run for a terminated non killed execution\\.","errorType":"validation","errorClass":"IllegalArgumentException","httpStatus":null,"severity":"error","filePath":"core/src/main/java/io/kestra/core/services/ExecutionService.java","lineNumber":554,"sourceCode":"        if (targetExecution.getState().canChangeStatus()) {\n            List<TaskRun> newTaskRuns = newExecution.getTaskRunList();\n            // We need to remove global error tasks and flowable error tasks if any\n            flow\n                .allErrorsWithChildren()\n                .forEach(task -> newTaskRuns.removeIf(taskRun -> taskRun.getTaskId().equals(task.getId())));\n\n            // We need to remove global finally tasks and flowable error tasks if any\n            flow\n                .allFinallyWithChildren()\n                .forEach(task -> newTaskRuns.removeIf(taskRun -> taskRun.getTaskId().equals(task.getId())));\n\n            // We need to remove afterExecution tasks\n            ListUtils.emptyOnNull(flow.getAfterExecution())\n                .forEach(task -> newTaskRuns.removeIf(taskRun -> taskRun.getTaskId().equals(task.getId())));\n\n            newExecution = newExecution.withTaskRunList(newTaskRuns);\n        } else {\n            throw new IllegalArgumentException(\"You can only change the state of a task run for a terminated non killed execution.\");\n        }\n\n        eventPublisher.publishEvent(CrudEvent.of(targetExecution, newExecution));\n        return newExecution;\n    }\n\n    /**\n     * Find the execution (main or loop sub-execution) that contains the given task run.\n     * Searches the given execution first; if not found, searches loop sub-executions.\n     *\n     * @param execution the parent execution to search first\n     * @param taskRunId the task run ID to find\n     * @return the execution and task run pair, or empty if not found in any execution\n     */\n    public Optional<ExecutionWithTaskRun> findExecutionWithTaskRun(Execution execution, String taskRunId) {\n        Optional<TaskRun> maybeTaskRun = ListUtils.emptyOnNull(execution.getTaskRunList()).stream()\n            .filter(tr -> tr.getId().equals(taskRunId))\n            .findFirst();","sourceCodeStart":536,"sourceCodeEnd":572,"githubUrl":"https://github.com/kestra-io/kestra/blob/823fada9274c4f9c251ea0a516460a4f7d958032/core/src/main/java/io/kestra/core/services/ExecutionService.java#L536-L572","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nexecutionService.changeTaskRunState(execution, flow, taskRunId, State.Type.SUCCESS);\n\n// after\nif (!execution.getState().canChangeStatus()) {\n    throw new IllegalStateException(\n        \"Cannot change task run state: execution is %s.\".formatted(execution.getState().getCurrent())\n    );\n}\nexecutionService.changeTaskRunState(execution, flow, taskRunId, State.Type.SUCCESS);","handlingStrategy":"validation","validationCode":"import io.kestra.core.models.executions.Execution;\n\nExecution fresh = executionRepository.findById(tenantId, executionId).orElseThrow();\nif (!fresh.getState().canChangeStatus()) {\n    throw new IllegalStateException(\n        \"Execution %s is in state %s; changeTaskRunState requires a terminated, non-killed execution.\".formatted(\n            fresh.getId(), fresh.getState().getCurrent())\n    );\n}\nexecutionService.changeTaskRunState(fresh, flow, taskRunId, newState);","typeGuard":"// Java guard helper — gate any changeTaskRunState call through this.\npublic static boolean canChangeTaskRunState(Execution execution) {\n    return execution != null && execution.getState().canChangeStatus();\n}","tryCatchPattern":"try {\n    executionService.changeTaskRunState(execution, flow, taskRunId, newState);\n} catch (IllegalArgumentException e) {\n    // re-fetch, surface a 409 Conflict with the current execution state,\n    // and do NOT retry unchanged — the state machine must be reconciled first.\n    throw new ConflictException(\"Execution %s cannot have its task run state changed: %s\".formatted(\n        execution.getId(), e.getMessage()));\n}","preventionTips":["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."],"tags":["execution","state-machine","task-run","kestra-core"],"backgroundTag":null,"analyzedSha":"823fada9274c4f9c251ea0a516460a4f7d958032","analyzedAt":"2026-08-14T06:15:17.947Z","schemaVersion":2},"datasetVersion":"2026-08-14T10:17:34.591Z"}