Branching & Loops¶
This guide explains how to implement conditional branching and loop patterns in Yagra workflows.
Conditional Branching¶
Conditional branching lets you route execution to different nodes based on runtime conditions.
Basic Branching Pattern¶
Workflow Structure:
A classifier node evaluates state and decides the next path
Conditional edges connect the classifier to different target nodes
The classifier returns
__next__with the branch label
Example: FAQ vs General Query Routing
nodes:
- id: "classifier"
handler: "classify_intent"
- id: "faq_bot"
handler: "answer_faq"
- id: "general_bot"
handler: "answer_general"
- id: "finish"
handler: "finish"
edges:
- source: "classifier"
target: "faq_bot"
condition: "faq"
- source: "classifier"
target: "general_bot"
condition: "general"
- source: "faq_bot"
target: "finish"
- source: "general_bot"
target: "finish"
Handler Implementation:
def classify_intent(state: AgentState, params: dict) -> dict:
query = state.get("query", "")
if "pricing" in query.lower() or "料金" in query:
intent = "faq"
else:
intent = "general"
return {"intent": intent, "__next__": intent}
Key Points:
The handler must return
{"__next__": "<branch_label>"}Branch labels must match
conditionvalues in edgesEach branch must have a corresponding edge
Multi-Way Branching¶
You can have more than two branches:
nodes:
- id: "router"
handler: "route_request"
- id: "sales"
handler: "handle_sales"
- id: "support"
handler: "handle_support"
- id: "billing"
handler: "handle_billing"
- id: "finish"
handler: "finish"
edges:
- source: "router"
target: "sales"
condition: "sales"
- source: "router"
target: "support"
condition: "support"
- source: "router"
target: "billing"
condition: "billing"
- source: "sales"
target: "finish"
- source: "support"
target: "finish"
- source: "billing"
target: "finish"
def route_request(state: AgentState, params: dict) -> dict:
query = state.get("query", "").lower()
if "buy" in query or "purchase" in query:
route = "sales"
elif "help" in query or "問題" in query:
route = "support"
elif "invoice" in query or "請求" in query:
route = "billing"
else:
route = "support" # Default
return {"route": route, "__next__": route}
Loop Patterns¶
Loops enable iterative workflows where execution returns to a previous node based on conditions.
Basic Loop: Planner → Evaluator¶
A common pattern for iterative refinement:
Planner generates a plan
Evaluator checks if plan is good enough
If not good enough, return to Planner (loop)
If good enough, proceed to Finish
Workflow:
nodes:
- id: "planner"
handler: "generate_plan"
- id: "evaluator"
handler: "evaluate_plan"
- id: "finish"
handler: "finalize"
edges:
- source: "planner"
target: "evaluator"
- source: "evaluator"
target: "planner"
condition: "retry"
- source: "evaluator"
target: "finish"
condition: "done"
Handlers:
def generate_plan(state: AgentState, params: dict) -> dict:
iteration = state.get("iteration", 0)
# Generate plan (possibly improved if iteration > 0)
plan = f"Plan v{iteration + 1}"
return {
"plan": plan,
"iteration": iteration + 1,
}
def evaluate_plan(state: AgentState, params: dict) -> dict:
iteration = state.get("iteration", 0)
max_iterations = params.get("max_iterations", 3)
# Check quality (simplified)
plan = state.get("plan", "")
is_good = len(plan) > 20 # Dummy criteria
if is_good or iteration >= max_iterations:
return {"__next__": "done"}
else:
return {"__next__": "retry"}
Loop with State Accumulation¶
Track history across iterations:
def generate_plan(state: AgentState, params: dict) -> dict:
iteration = state.get("iteration", 0)
history = state.get("history", [])
previous_feedback = history[-1] if history else None
# Generate improved plan based on feedback
plan = f"Plan v{iteration + 1}"
if previous_feedback:
plan += f" (improved from: {previous_feedback})"
return {
"plan": plan,
"iteration": iteration + 1,
}
def evaluate_plan(state: AgentState, params: dict) -> dict:
iteration = state.get("iteration", 0)
max_iterations = params.get("max_iterations", 3)
history = state.get("history", [])
plan = state.get("plan", "")
# Evaluate and provide feedback
is_good = iteration >= 2 # Simplified
if is_good:
return {"__next__": "done"}
else:
feedback = "Plan needs more detail"
return {
"history": history + [feedback],
"__next__": "retry",
}
Preventing Infinite Loops¶
Always include a termination condition to avoid infinite loops:
Max iterations: Exit after N iterations
Quality threshold: Exit when quality is acceptable
Timeout: (Not directly supported in Yagra—implement in handler)
Example:
def evaluator(state: AgentState, params: dict) -> dict:
iteration = state.get("iteration", 0)
max_iterations = params.get("max_iterations", 5)
if iteration >= max_iterations:
return {"__next__": "done"} # Force exit
# ... quality check ...
Advanced Patterns¶
Nested Branching¶
Combine branches within branches:
nodes:
- id: "intent_classifier"
handler: "classify_intent"
- id: "faq_router"
handler: "route_faq"
- id: "pricing_bot"
handler: "answer_pricing"
- id: "feature_bot"
handler: "answer_features"
- id: "general_bot"
handler: "answer_general"
- id: "finish"
handler: "finish"
edges:
# First level: Intent classification
- source: "intent_classifier"
target: "faq_router"
condition: "faq"
- source: "intent_classifier"
target: "general_bot"
condition: "general"
# Second level: FAQ subcategories
- source: "faq_router"
target: "pricing_bot"
condition: "pricing"
- source: "faq_router"
target: "feature_bot"
condition: "features"
# All paths converge to finish
- source: "pricing_bot"
target: "finish"
- source: "feature_bot"
target: "finish"
- source: "general_bot"
target: "finish"
Loop with Branch Exit¶
Combine loops and branches:
nodes:
- id: "generator"
handler: "generate_content"
- id: "reviewer"
handler: "review_content"
- id: "approver"
handler: "approve_content"
- id: "finish"
handler: "finalize"
edges:
- source: "generator"
target: "reviewer"
- source: "reviewer"
target: "generator"
condition: "needs_revision"
- source: "reviewer"
target: "approver"
condition: "ready_for_approval"
- source: "approver"
target: "generator"
condition: "rejected"
- source: "approver"
target: "finish"
condition: "approved"
Visualizing Branching and Loops¶
Use yagra studio for interactive visualization and editing:
yagra studio --workflow workflows/loop.yaml --port 8787
Best Practices¶
Branching¶
Explicit labels: Use descriptive condition names (
"needs_revision"not"0")Default fallback: Handle unexpected cases (e.g., default to
"general")Validate branches: Ensure all conditions have corresponding edges
Loops¶
Set max iterations: Always include a termination condition
Track progress: Use
iterationorattempt_countin stateProvide feedback: Pass quality feedback to improve next iteration
Monitor cost: Each iteration may call expensive LLM APIs
Debugging¶
Add logging: Print
__next__value in handlersValidate workflow: Use
yagra validate --format jsonto catch edge errorsVisualize: Generate HTML to visually verify flow
Common Pitfalls¶
Missing __next__ in Branching Node¶
Error: LangGraph raises an error because it doesn’t know which edge to take.
Solution: Always return __next__ in branching nodes:
def classifier(state: AgentState, params: dict) -> dict:
intent = "faq" if "pricing" in state["query"] else "general"
return {"intent": intent, "__next__": intent} # ✅
Infinite Loop Without Exit¶
Symptom: Workflow runs forever or times out.
Solution: Add max iteration check:
iteration = state.get("iteration", 0)
if iteration >= max_iterations:
return {"__next__": "done"} # Force exit
Mismatched Condition Labels¶
Error: ValidationError: condition 'xyz' has no corresponding edge
Solution: Ensure edge condition matches __next__ value:
edges:
- source: "evaluator"
target: "finish"
condition: "done" # Must match __next__ value
return {"__next__": "done"} # ✅ Matches
Examples¶
See examples/workflows/ for complete examples:
branch-inline.yaml: Simple branchingloop-split.yaml: Loop with conditional exitrag.yaml: Multi-stage RAG pipeline