Skip to content

Bug: Workflow._build_workflow KeyError on if_else branch when user authored only one side (true OR false) #248

Description

@Fr3ya

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions