Describe the bug
Workflow._build_workflow in api/internal/core/workflow/workflow.py#L188-L225 assembles the branches dict for graph.add_conditional_edges from whatever edges the user authored leaving an if_else node:
# workflow.py:188-200 (simplified)
conditional_edges = {}
for edge in edges:
if edge.source_type == NodeType.IF_ELSE.value:
if source_node not in conditional_edges:
conditional_edges[source_node] = {}
branch = edge.source_handle if edge.source_handle is not None else "true"
conditional_edges[source_node][branch] = target_node
And the routing function returns either "true" or "false" unconditionally based on the if_else node's result:
# workflow.py:213-219
def condition_func(state: WorkflowState):
for node_result in reversed(state.get("node_results", [])):
if f"{node_result.node_data.node_type}_{node_result.node_data.id}" == node_id:
result = node_result.outputs.get("result", True)
return "true" if result else "false"
return "true"
If a user authors a workflow with only the true branch from an if_else (a common UX pattern where the implicit semantics is "go to T on true, terminate on false") — or symmetrically only the false branch — the branches dict for that if_else node has exactly one key. At runtime the if_else node evaluates, condition_func returns the opposite string, and graph.add_conditional_edges raises KeyError because the routing function returned a key that was not declared in the path_map.
To Reproduce
Self-contained reproducer — no openagent install required. Mirrors WorkflowState shape (with the same reducers) plus the exact condition_func factory and branches dict assembly from workflow.py. Requires only langgraph.
Save as repro.py and run python repro.py:
import asyncio
from dataclasses import dataclass
from typing import Annotated, List, TypedDict
from langgraph.graph import END, START, StateGraph
def _process_dict(left, right):
return {**(left or {}), **(right or {})}
def _process_node_results(left, right):
return (left or []) + (right or [])
@dataclass
class NodeResultData: node_type: str; id: str
@dataclass
class NodeResult: node_data: NodeResultData; outputs: dict
class WorkflowState(TypedDict, total=False):
inputs: Annotated[dict, _process_dict]
outputs: Annotated[dict, _process_dict]
node_results: Annotated[List[NodeResult], _process_node_results]
# Faithful port of openagent's condition_func factory
def create_condition_func(if_else_node_id):
def condition_func(state):
for nr in reversed(state.get("node_results", [])):
if f"{nr.node_data.node_type}_{nr.node_data.id}" == if_else_node_id:
result = nr.outputs.get("result", True)
return "true" if result else "false"
return "true"
return condition_func
async def if_else_node(state):
return {"node_results": [
NodeResult(NodeResultData("if_else", "A"), {"result": False})
]}
async def true_branch_node(state):
return {"outputs": {"reached": "true_branch"}}
async def main():
g = StateGraph(WorkflowState)
g.add_node("if_else_A", if_else_node)
g.add_node("template_transform_T", true_branch_node)
g.add_edge(START, "if_else_A")
g.add_edge("template_transform_T", END)
# User wired only the `true` branch. openagent's edge-assembly loop
# produces this dict exactly:
branches = {"true": "template_transform_T"}
g.add_conditional_edges("if_else_A", create_condition_func("if_else_A"), branches)
compiled = g.compile()
try:
await compiled.ainvoke({"inputs": {}})
except Exception as e:
print(f"FAILED: {type(e).__name__}: {e}")
asyncio.run(main())
Output:
FAILED: KeyError: 'false'
Expected behavior
A workflow with a partially-wired if_else node should either (a) reject the configuration at workflow-validation time with a clear "if_else node missing branch" error, or (b) silently route the missing branch to END (matching the user's likely intent of "terminate on this branch").
Actual behavior
The workflow compiles successfully and starts executing. The moment an if_else node evaluates to the side the user didn't wire, the graph raises KeyError: 'false' (or KeyError: 'true') deep inside LangGraph's branch resolution.
Describe the bug
Workflow._build_workflowinapi/internal/core/workflow/workflow.py#L188-L225assembles thebranchesdict forgraph.add_conditional_edgesfrom whatever edges the user authored leaving anif_elsenode:And the routing function returns either
"true"or"false"unconditionally based on the if_else node'sresult:If a user authors a workflow with only the
truebranch from anif_else(a common UX pattern where the implicit semantics is "go to T on true, terminate on false") — or symmetrically only thefalsebranch — thebranchesdict for that if_else node has exactly one key. At runtime the if_else node evaluates,condition_funcreturns the opposite string, andgraph.add_conditional_edgesraisesKeyErrorbecause the routing function returned a key that was not declared in the path_map.To Reproduce
Self-contained reproducer — no openagent install required. Mirrors
WorkflowStateshape (with the same reducers) plus the exactcondition_funcfactory andbranchesdict assembly fromworkflow.py. Requires onlylanggraph.Save as
repro.pyand runpython repro.py:Output:
Expected behavior
A workflow with a partially-wired if_else node should either (a) reject the configuration at workflow-validation time with a clear "if_else node missing branch" error, or (b) silently route the missing branch to
END(matching the user's likely intent of "terminate on this branch").Actual behavior
The workflow compiles successfully and starts executing. The moment an if_else node evaluates to the side the user didn't wire, the graph raises
KeyError: 'false'(orKeyError: 'true') deep inside LangGraph's branch resolution.