conductor-oss/conductor · error · TerminateWorkflowException

Fork task definition is not followed by a join task. Check

Error message

Fork task definition is not followed by a join task.  Check the blueprint

What it means

Thrown by ForkJoinTaskMapper (for static FORK_JOIN tasks, not dynamic) when the task following the FORK task in the workflow definition is not a JOIN. The mapper calls getNextTask() on the fork's taskReferenceName and checks if the result is a JOIN task. If null (no next task) or the type is not 'JOIN', the workflow is terminated. This is purely a workflow definition error.

Source

Thrown at core/src/main/java/com/netflix/conductor/core/execution/mapper/ForkJoinTaskMapper.java:104

        tasksToBeScheduled.add(forkTask);
        List<List<WorkflowTask>> forkTasks = workflowTask.getForkTasks();
        for (List<WorkflowTask> wfts : forkTasks) {
            WorkflowTask wft = wfts.get(0);
            List<TaskModel> tasks2 =
                    taskMapperContext
                            .getDeciderService()
                            .getTasksToBeScheduled(workflowModel, wft, retryCount);
            tasksToBeScheduled.addAll(tasks2);
        }

        WorkflowTask joinWorkflowTask =
                workflowModel
                        .getWorkflowDefinition()
                        .getNextTask(workflowTask.getTaskReferenceName());

        if (joinWorkflowTask == null || !joinWorkflowTask.getType().equals(TaskType.JOIN.name())) {
            throw new TerminateWorkflowException(
                    "Fork task definition is not followed by a join task.  Check the blueprint");
        }
        List<TaskModel> joinTask =
                taskMapperContext
                        .getDeciderService()
                        .getTasksToBeScheduled(workflowModel, joinWorkflowTask, retryCount);

        tasksToBeScheduled.addAll(joinTask);
        return tasksToBeScheduled;
    }
}

View on GitHub (pinned to cf7c3e4a8a)

Solutions

  1. Add a JOIN task immediately after every FORK_JOIN task in the workflow definition.
  2. Ensure the JOIN task's type field is exactly 'JOIN'.
  3. Validate the workflow definition before registration using the Conductor metadata API or UI.
  4. Check that the forkTasks structure in the definition is well-formed — each branch is a non-empty list.

Example fix

// before
[
  { "type": "FORK_JOIN", "taskReferenceName": "fork1",
    "forkTasks": [[{"type":"SIMPLE","taskReferenceName":"a"}]] },
  { "type": "SIMPLE", "taskReferenceName": "after" }
]

// after
[
  { "type": "FORK_JOIN", "taskReferenceName": "fork1",
    "forkTasks": [[{"type":"SIMPLE","taskReferenceName":"a"}]] },
  { "type": "JOIN", "taskReferenceName": "join1", "joinOn": ["a"] },
  { "type": "SIMPLE", "taskReferenceName": "after" }
]
Defensive patterns

Strategy: validation

Validate before calling

// Validate: FORK_JOIN must be followed by JOIN
WorkflowDef def = metadataDAO.getWorkflowDef(name, version);
for (WorkflowTask wt : def.getTasks()) {
    if (TaskType.TASK_TYPE_FORK_JOIN.equals(wt.getType())) {
        WorkflowTask next = def.getNextTask(wt.getTaskReferenceName());
        if (next == null || !TaskType.JOIN.name().equals(next.getType())) {
            throw new IllegalStateException(
                "FORK_JOIN " + wt.getTaskReferenceName()
                + " must be followed by a JOIN task");
        }
    }
}

Prevention

When it happens

Trigger: A FORK_JOIN task in the workflow definition is followed by a non-JOIN task, or is the last task. The JOIN was omitted, reordered, or given the wrong type string.

Common situations: Workflow blueprint was hand-edited and the JOIN after a fork was accidentally deleted. A fork was inserted mid-sequence without adding a corresponding JOIN. The JOIN task type is misspelled or has wrong casing.

Related errors


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