Description
Flows using conditional branching (self.next({...}, condition=...)) hang indefinitely on Argo Workflows v3.7.11+ and v4.0.2+. The convergence step after conditional branches is never scheduled.
Reproducer
The metaflow-music-agent example reproduces this. The ResearchAgentFlow uses:
self.next(
{
"continue": self.research,
"done": self.done,
"unknown": self.unknown,
"failed": self.failed,
},
condition="decision",
)
done, unknown, and failed all converge on agent_complete -> join -> end. Only one branch executes per foreach iteration; the others are skipped.
Run with:
python agentflow.py --environment=pypi --with kubernetes run --max-artists 2
The research steps complete successfully, each branch reaches done or unknown, but agent_complete is never scheduled and the workflow hangs in Running state.
Root Cause
Two interacting issues:
1. How Metaflow generates input-paths for conditional join steps
For a convergence step like agent_complete, the generated Argo DAG template references outputs from all conditional branches unconditionally:
arguments:
parameters:
- name: input-paths
value: "argo-{{workflow.name}}/:done/{{tasks.done.outputs.parameters.task-id}},failed/{{tasks.failed.outputs.parameters.task-id}},unknown/{{tasks.unknown.outputs.parameters.task-id}}"
depends: "done.Succeeded || failed.Succeeded || unknown.Succeeded"
The depends clause is correct (OR), but input-paths references task-id from all three branches. Skipped branches produce no outputs.
Metaflow's intended workaround (introduced in #2560) is to base64-encode the value and filter out unresolved paths at runtime via conditional_input_paths.py. This depends on Argo passing through unresolved {{tasks.X...}} expressions as literal strings.
2. Argo Workflows changed parameter resolution behavior
Argo PR argoproj/argo-workflows#15442 ("fix: requeue workflow if expected variables are missing", Feb 26 2026) changed dag.go from:
// Before (v3.7.10, v4.0.1 and earlier) — unresolved expressions passed through
newTaskStr, err := template.Replace(ctx, string(taskBytes), mergedParams, true)
to:
// After (v3.7.11, v4.0.2+) — unresolved tasks.*/steps.* references cause requeue
newTaskStr, err := template.ReplaceStrict(ctx, string(taskBytes), mergedParams, []string{"tasks", "steps"})
if err != nil {
if template.IsMissingVariableErr(err) {
woc.requeue()
return nil, ErrRequeue // pod never created, runtime filter never runs
}
}
The controller now requeues indefinitely when it can't resolve {{tasks.failed.outputs.parameters.task-id}} for a skipped task. The conditional_input_paths.py runtime filter never gets a chance to execute.
Controller logs confirm this
level=WARN msg="was unable to find variable" error="failed to resolve {{tasks.failed.outputs.parameters.task-id}}"
level=WARN msg="was unable to find variable" error="failed to resolve {{tasks.done.outputs.parameters.task-id}}"
level=WARN msg="was unable to obtain the node" taskName=agent-complete
Affected Versions
| Argo Version |
Behavior |
Conditionals work? |
| v3.5.x - v3.7.10 |
Replace(allowUnresolved=true) |
Yes |
| v3.7.11+ |
ReplaceStrict |
No |
| v4.0.0 - v4.0.1 |
Replace(allowUnresolved=true) |
Yes |
| v4.0.2+ |
ReplaceStrict |
No |
Tested with Metaflow 2.19.22. Likely affects all versions since #2560 (Aug 2025).
History
For context, the original conditionals PR (#2550, Aug 19 2025) used safe Argo expressions that handled skipped tasks correctly:
{{=(get(tasks['done']?.outputs?.parameters, 'task-id') ?? 'no-task')}}
This was removed two days later in #2560 (Aug 21 2025) because ?. optional chaining doesn't work inside foreach (withParam nested DAGs evaluate all ?. as false). The replacement strategy relied on Argo's passthrough behavior, which is no longer guaranteed.
Argo-side tracking
This is related to argoproj/argo-workflows#15737. A fix (PR #15736) has been merged and cherry-picked to v4.0 and v3.7, but that fix targets expanded task properties (withItems/withParams). It may not fully cover the skipped conditional branch case — worth verifying once v3.7.12 / v4.0.4 are released.
Workaround
Downgrade Argo Workflows to v3.7.10 or v4.0.1.
Environment
- Metaflow: 2.19.22
- Argo Workflows: tested on v4.0.3 and v3.7.11
- Kubernetes: EKS
- Python: 3.12
Description
Flows using conditional branching (
self.next({...}, condition=...)) hang indefinitely on Argo Workflows v3.7.11+ and v4.0.2+. The convergence step after conditional branches is never scheduled.Reproducer
The metaflow-music-agent example reproduces this. The
ResearchAgentFlowuses:done,unknown, andfailedall converge onagent_complete->join->end. Only one branch executes per foreach iteration; the others are skipped.Run with:
The research steps complete successfully, each branch reaches
doneorunknown, butagent_completeis never scheduled and the workflow hangs inRunningstate.Root Cause
Two interacting issues:
1. How Metaflow generates
input-pathsfor conditional join stepsFor a convergence step like
agent_complete, the generated Argo DAG template references outputs from all conditional branches unconditionally:The
dependsclause is correct (OR), butinput-pathsreferencestask-idfrom all three branches. Skipped branches produce no outputs.Metaflow's intended workaround (introduced in #2560) is to base64-encode the value and filter out unresolved paths at runtime via
conditional_input_paths.py. This depends on Argo passing through unresolved{{tasks.X...}}expressions as literal strings.2. Argo Workflows changed parameter resolution behavior
Argo PR argoproj/argo-workflows#15442 ("fix: requeue workflow if expected variables are missing", Feb 26 2026) changed
dag.gofrom:to:
The controller now requeues indefinitely when it can't resolve
{{tasks.failed.outputs.parameters.task-id}}for a skipped task. Theconditional_input_paths.pyruntime filter never gets a chance to execute.Controller logs confirm this
Affected Versions
Replace(allowUnresolved=true)ReplaceStrictReplace(allowUnresolved=true)ReplaceStrictTested with Metaflow 2.19.22. Likely affects all versions since #2560 (Aug 2025).
History
For context, the original conditionals PR (#2550, Aug 19 2025) used safe Argo expressions that handled skipped tasks correctly:
This was removed two days later in #2560 (Aug 21 2025) because
?.optional chaining doesn't work inside foreach (withParamnested DAGs evaluate all?.as false). The replacement strategy relied on Argo's passthrough behavior, which is no longer guaranteed.Argo-side tracking
This is related to argoproj/argo-workflows#15737. A fix (PR #15736) has been merged and cherry-picked to v4.0 and v3.7, but that fix targets expanded task properties (
withItems/withParams). It may not fully cover the skipped conditional branch case — worth verifying once v3.7.12 / v4.0.4 are released.Workaround
Downgrade Argo Workflows to v3.7.10 or v4.0.1.
Environment