conductor-oss/conductor · error · TerminateWorkflowException

Tasks could not be dynamically forked due to invalid input:

Error message

Tasks could not be dynamically forked due to invalid input: %s

What it means

Thrown by ForkJoinDynamicTaskMapper during the processing of dynamic fork task inputs. When merging forkedTaskInput into the dynamic task's inputParameters, any exception (ClassCastException, NullPointerException, etc.) is caught and wrapped into a TerminateWorkflowException with the original exception's message. This terminates the workflow — the fork cannot proceed with corrupt input.

Source

Thrown at core/src/main/java/com/netflix/conductor/core/execution/mapper/ForkJoinDynamicTaskMapper.java:213

            for (WorkflowTask dynForkTask :
                    dynForkTasks) { // TODO this is a cyclic dependency, break it out using function
                // composition
                try {
                    Map<String, Object> forkedTaskInput =
                            tasksInput.get(dynForkTask.getTaskReferenceName());
                    if (dynForkTask.getInputParameters() == null) {
                        dynForkTask.setInputParameters(new HashMap<>());
                    }
                    if (forkedTaskInput == null) {
                        forkedTaskInput = new HashMap<>();
                    }
                    dynForkTask.getInputParameters().putAll(forkedTaskInput);
                } catch (Exception e) {
                    String reason =
                            String.format(
                                    "Tasks could not be dynamically forked due to invalid input: %s",
                                    e.getMessage());
                    throw new TerminateWorkflowException(reason);
                }
                List<TaskModel> forkedTasks =
                        taskMapperContext
                                .getDeciderService()
                                .getTasksToBeScheduled(workflowModel, dynForkTask, retryCount);
                if (forkedTasks == null || forkedTasks.isEmpty()) {
                    Optional<String> existingTaskRefName =
                            workflowModel.getTasks().stream()
                                    .filter(
                                            runningTask ->
                                                    runningTask
                                                                    .getStatus()
                                                                    .equals(
                                                                            TaskModel.Status
                                                                                    .IN_PROGRESS)
                                                            || runningTask.getStatus().isTerminal())
                                    .map(TaskModel::getReferenceTaskName)
                                    .filter(

View on GitHub (pinned to cf7c3e4a8a)

Solutions

  1. Inspect the full ForkJoinDynamicTaskMapper error in the server log — the original exception's message is embedded in the TerminateWorkflowException reason.
  2. Verify that every entry in the dynamic fork tasks input map (keyed by taskReferenceName) is itself a Map<String, Object>.
  3. Fix the upstream task or input mapping so each forked task's input is a proper JSON object.

Example fix

// before — forkedTasksInput contains a scalar for ref 't1'
{
  "forkedTasks": [{ "name": "task1", "taskReferenceName": "t1" }],
  "forkedTasksInput": {
    "t1": "should-be-a-map"
  }
}

// after
{
  "forkedTasks": [{ "name": "task1", "taskReferenceName": "t1" }],
  "forkedTasksInput": {
    "t1": { "param": "value" }
  }
}
Defensive patterns

Strategy: validation

Validate before calling

// Validate forked task inputs are maps before processing
for (WorkflowTask dynForkTask : dynForkTasks) {
    Object forkedInput = tasksInput.get(dynForkTask.getTaskReferenceName());
    if (forkedInput != null && !(forkedInput instanceof Map)) {
        throw new IllegalArgumentException(
            "Input for " + dynForkTask.getTaskReferenceName()
            + " must be a Map, got: " + forkedInput.getClass());
    }
}

Type guard

private boolean allInputsAreMaps(Map<String, ?> tasksInput, List<WorkflowTask> tasks) {
    return tasks.stream()
        .map(WorkflowTask::getTaskReferenceName)
        .map(tasksInput::get)
        .allMatch(v -> v == null || v instanceof Map);
}

Prevention

When it happens

Trigger: A FORK_JOIN_DYNAMIC task whose dynamic task input map has unexpected types — e.g., forkedTaskInput is a List instead of a Map, or a ClassCastException occurs when calling putAll(). The input key referenced by the dynamic task's taskReferenceName does not map to a Map<String, Object>.

Common situations: The input parameter named by the forked task reference contains a non-map value at runtime. Upstream task output schema changed so a previously-map field is now a scalar. JSON deserialization produced a LinkedHashMap where a specific subtype was expected.

Related errors


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