conductor-oss/conductor · error · ConflictException

Workflow is in terminal state. Status = %s

Error message

Workflow is in terminal state. Status = %s

What it means

resetCallbacksForWorkflow loads the workflow and refuses to operate if its status is already terminal (COMPLETED, FAILED, TIMED_OUT, TERMINATED, ROLLED_BACK), throwing a ConflictException. Resetting callbacks on a finished workflow is a no-op-at-best and is rejected to avoid inconsistent state.

Source

Thrown at core/src/main/java/com/netflix/conductor/core/execution/WorkflowExecutorOps.java:144

        this.activeWorkerLastPollMs = properties.getActiveWorkerLastPollTimeout().toMillis();
        this.workflowStatusListener = workflowStatusListener;
        this.taskStatusListener = taskStatusListener;
        this.executionLockService = executionLockService;
        this.parametersUtils = parametersUtils;
        this.idGenerator = idGenerator;
        this.systemTaskRegistry = systemTaskRegistry;
        this.workflowMessageQueueDAO = workflowMessageQueueDAO;
    }

    /**
     * @param workflowId the id of the workflow for which task callbacks are to be reset
     * @throws ConflictException if the workflow is in terminal state
     */
    @Override
    public void resetCallbacksForWorkflow(String workflowId) {
        WorkflowModel workflow = executionDAOFacade.getWorkflowModel(workflowId, true);
        if (workflow.getStatus().isTerminal()) {
            throw new ConflictException(
                    "Workflow is in terminal state. Status = %s", workflow.getStatus());
        }

        // Get SIMPLE tasks in SCHEDULED state that have callbackAfterSeconds > 0 and
        // set the
        // callbackAfterSeconds to 0
        workflow.getTasks().stream()
                .filter(
                        task ->
                                !systemTaskRegistry.isSystemTask(task.getTaskType())
                                        && SCHEDULED == task.getStatus()
                                        && task.getCallbackAfterSeconds() > 0)
                .forEach(
                        task -> {
                            if (queueDAO.resetOffsetTime(
                                    QueueUtils.getQueueName(task), task.getTaskId())) {
                                task.setCallbackAfterSeconds(0);
                                executionDAOFacade.updateTask(task);

View on GitHub (pinned to cf7c3e4a8a)

Solutions

  1. Check workflow.getStatus().isTerminal() before calling resetCallbacksForWorkflow.
  2. If the workflow is terminal and you need to re-run, use restart or rerun instead of reset.
  3. Make the calling automation idempotent and skip already-terminal workflows.

Example fix

// before
workflowExecutor.resetCallbacksForWorkflow(workflowId);

// after
WorkflowModel wf = executionDAOFacade.getWorkflowModel(workflowId, true);
if (!wf.getStatus().isTerminal()) {
    workflowExecutor.resetCallbacksForWorkflow(workflowId);
}
Defensive patterns

Strategy: validation

Validate before calling

WorkflowModel wf = executionDAOFacade.getWorkflowModel(workflowId, true);
if (wf.getStatus().isTerminal()) {
    // skip reset; surface to caller or use restart/rerun
    return;
}
workflowExecutor.resetCallbacksForWorkflow(workflowId);

Try / catch

try {
    workflowExecutor.resetCallbacksForWorkflow(workflowId);
} catch (ConflictException e) {
    // workflow already terminal; use restart/rerun instead
}

Prevention

When it happens

Trigger: Calling the reset-callbacks API (or WorkflowExecutor.resetCallbacksForWorkflow) on a workflow that has already reached a terminal status.

Common situations: Retrying a reset after the workflow finished in the meantime; automation that polls and resets without checking status; UI button enabled on a completed workflow.

Related errors


AI-assisted analysis of conductor-oss/conductor@cf7c3e4a8a (2026-08-14). Data as JSON: /api/errors/0b054c8f29522691. Report an issue: GitHub.