conductor-oss/conductor · error · TerminateWorkflowException

%s

Error message

%s

What it means

isTaskSkipped wraps its entire body in try/catch(Exception) and rethrows any exception's message as a TerminateWorkflowException with no cause and no structured status. The %s is just the inner exception's getMessage(). This obscures the real error type and makes the failure look like a workflow-termination when it may be a data-resolution bug (e.g. getTaskByRefName failing).

Source

Thrown at core/src/main/java/com/netflix/conductor/core/execution/DeciderService.java:1041

    private int applyMaxRetryDelayCap(int delaySeconds, TaskDef taskDef) {
        int cap = taskDef.getMaxRetryDelaySeconds();
        return (cap > 0 && delaySeconds > cap) ? cap : delaySeconds;
    }

    private boolean isTaskSkipped(WorkflowTask taskToSchedule, WorkflowModel workflow) {
        try {
            boolean isTaskSkipped = false;
            if (taskToSchedule != null) {
                TaskModel t = workflow.getTaskByRefName(taskToSchedule.getTaskReferenceName());
                if (t == null) {
                    isTaskSkipped = false;
                } else if (t.getStatus().equals(SKIPPED)) {
                    isTaskSkipped = true;
                }
            }
            return isTaskSkipped;
        } catch (Exception e) {
            throw new TerminateWorkflowException(e.getMessage());
        }
    }

    private boolean isAyncCompleteSystemTask(TaskModel task) {
        return systemTaskRegistry.isSystemTask(task.getTaskType())
                && systemTaskRegistry.get(task.getTaskType()).isAsyncComplete(task);
    }

    public static class DeciderOutcome {

        List<TaskModel> tasksToBeScheduled = new LinkedList<>();
        List<TaskModel> tasksToBeUpdated = new LinkedList<>();
        boolean isComplete;
        TaskModel terminateTask;

        private DeciderOutcome() {}
    }
}

View on GitHub (pinned to cf7c3e4a8a)

Solutions

  1. Search server logs for the full stack trace of the wrapped exception (the message alone is insufficient).
  2. Validate that all WorkflowTask.taskReferenceName values in the definition are unique and non-null.
  3. Reproduce with the same workflow definition and inspect getTaskByRefName behavior.
  4. Consider improving the catch to preserve the cause — file an issue, since the current swallow loses diagnosability.
Defensive patterns

Strategy: try-catch

Validate before calling

// Validate unique, non-null taskReferenceNames at definition time.
Set<String> refs = new HashSet<>();
for (WorkflowTask t : def.getTasks()) {
    if (t.getTaskReferenceName() == null || !refs.add(t.getTaskReferenceName())) {
        throw new IllegalArgumentException("Duplicate/null taskReferenceName in " + def.getName());
    }
}

Try / catch

try {
    deciderService.decide(workflow);
} catch (TerminateWorkflowException e) {
    // message is opaque; pull full stack trace from server logs
    LOGGER.error("isTaskSkipped wrapped exception: {}", e.getMessage());
}

Prevention

When it happens

Trigger: workflow.getTaskByRefName(refName) throws inside isTaskSkipped during startWorkflow's skip-loop — e.g. duplicate reference names, corrupt task data, or a null WorkflowTask reference name.

Common situations: Two WorkflowTasks share the same taskReferenceName; a workflow model with corrupted task list; a null taskReferenceName on the task to schedule.

Related errors


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