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
- Add a JOIN task immediately after every FORK_JOIN task in the workflow definition.
- Ensure the JOIN task's type field is exactly 'JOIN'.
- Validate the workflow definition before registration using the Conductor metadata API or UI.
- 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
- Always pair every FORK_JOIN with a subsequent JOIN task.
- Validate the workflow definition structure before registration.
- Use the Conductor UI blueprint view to visually verify fork-join pairing.
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
- Dynamic join definition is not followed by a join task. Che
- Workflow: %s, version: %s already exists!
- Error creating workflow definition: %s/%d
- Error updating workflow definition: %s/%d
- Error fetching workflow def: %s/%d
AI-assisted analysis of conductor-oss/conductor@cf7c3e4a8a (2026-08-14).
Data as JSON: /api/errors/39bef56b6fdf771e.
Report an issue: GitHub.