Tutorials Logic, IN info@tutorialslogic.com

LangGraph Conditional Workflows: Routing, Loops, and Controlled Branching

Routing Functions Should Be Tiny and Boring

A graph becomes interesting the moment the next step is not fixed. Conditional routing is where LangGraph stops being a linear pipeline builder and starts becoming a real orchestration tool.

Routing lets the graph send work to different nodes based on category, confidence, validation results, approval state, tool output, or loop counters. That power is exactly why the design must stay disciplined. Uncontrolled routing creates the most expensive bugs in agent systems.

This page focuses on one principle: branching should clarify the workflow, not turn it into a maze.

A router is healthiest when it reads a few fields and returns one of a small set of node names. If the route function grows into a mini-application, split the work into scoring or preparation nodes first.

This is especially important for production. A short route function is easy to unit test with plain state dictionaries, which makes incident triage faster later.

  • Route by category when classification is already known.
  • Route by validation outcome after a checker node.
  • Route by retry limit when a loop must stop safely.

Loops Need Counters, Exit Criteria, and a Graceful Failure Path

Loops are normal in research, coding, extraction, and tool-calling graphs. They become dangerous when the graph has no explicit limit or no alternate destination after repeated failure.

Every loop should answer three operational questions: how many times may it repeat, what evidence ends it successfully, and where does it go when success never arrives?

  • Store `attempts`, `retry_count`, or `step_count` in state.
  • Route to END when quality is good enough.
  • Route to human review or a failure summary when limits are exhausted.

Branch Design for Human Readability

A graph is a communication artifact as much as an execution artifact. When you choose route names like `needs_human_review`, `retry_search`, or `publish`, future readers understand the business process instantly.

Compare that with opaque route labels like `path_a` and `path_b`. Those save seconds while writing and cost hours during debugging.

  • Prefer semantic route names.
  • Keep one routing strategy per node.
  • Document the state fields that drive each route.

Execution Flow Analysis for a Retry Graph

Consider a retrieval workflow: search, draft, evaluate, retry if evidence is weak. The graph is not merely repeating. It is making a stateful decision at each evaluation checkpoint.

That means the evaluation node is a business-control boundary. It determines whether the cost of another iteration is worth paying.

  • Start state: question, attempts=0
  • search node adds documents
  • draft node adds answer
  • evaluate node adds quality and increments attempts
  • router sends run to END, retry, or human review

When to Use Command Instead of add_conditional_edges

Current docs highlight `Command` for cases where a node naturally needs to update state and choose the next hop in one step. It keeps the graph concise when separating mutation from routing would add ceremony without clarity.

Use conditional edges when you want a distinct routing function that can be tested independently. Use `Command` when the node itself is the decision boundary.

  • `add_conditional_edges` emphasizes explicit routing logic.
  • `Command` emphasizes one return value containing update plus goto.
  • Choose the style that keeps the decision easiest to read and test.

Design Conditional Routes Like Product Decisions

A conditional edge is more than a branch in code. It is a product decision encoded in the graph. The route might decide whether to retrieve more evidence, call a tool, ask a human, retry after an error, or finish. Because route decisions control the workflow, they should be small, deterministic where possible, and easy to test with minimal state examples.

Good routing starts with clear route labels. Use semantic outcomes such as `needs_more_evidence`, `requires_approval`, `ready_to_answer`, or `tool_failed_retryable`. Avoid vague labels like `next`, `other`, or `continue` because they make traces harder to read and tests harder to understand.

Loops require special care. A loop without a counter, progress check, or stop condition can become an expensive failure. Store retry counts, search attempts, or completed steps in state, and route to fallback or escalation when progress stops. Controlled loops are one of the biggest differences between a reliable graph and an improvising agent.

Conditional workflows should also include negative paths. What happens when evidence is missing, confidence is low, policy blocks action, a tool fails, or the user cancels? If every branch assumes success, the graph is not production-ready.

  • Name routes after meaningful workflow outcomes.
  • Keep route functions small and fixture-testable.
  • Add counters and progress checks to loops.
  • Represent missing evidence, denial, failure, and cancellation explicitly.

Route Matrix Exercise

Design a route table for one workflow. List each route name, the state fields it reads, the condition that triggers it, the next node, and the safe fallback. This makes branching behavior reviewable before you write code.

Then test the route function with tiny state fixtures. Include success, missing data, low confidence, tool failure, approval required, and max-loop cases. If a route cannot be tested without a full model call, it is probably doing too much.

  • Write route tables before graph assembly.
  • Test route functions with minimal state.
  • Include fallback and loop-limit cases.

Route Trace Clarity

Route behavior should be understandable from the trace alone. If a reviewer cannot tell why a branch was chosen, add clearer state fields or route labels.

Route Change Review

Compare route coverage before and after a routing change. Save the state fixture, chosen label, next node, and stop outcome so a refactor cannot silently redirect important cases.

Routing Contract

A route function should read a small documented subset of state and return one of a closed set of destinations. Keep model parsing, network calls, and side effects in nodes; a route should be deterministic enough to test with tiny fixtures. Map semantic outcomes such as `approved`, `needs_evidence`, and `stop` to node names instead of letting arbitrary model text become a destination.

Use conditional edges when routing follows a completed node update. Use `Command` when one node should return both an update and the next destination. Use `Send` for dynamic fan-out such as processing an unknown number of documents, then merge parallel updates through reducers. Each option should make ownership clearer, not merely reduce lines of code.

Every loop needs progress state and a terminal policy. Track attempts, visited work, last error, elapsed budget, or evidence gained; stop on success, refusal, escalation, maximum steps, or no progress. A recursion limit is a final containment mechanism, not the primary definition of completion.

Emit a route label and the state fields that justified it into privacy-safe trace metadata. When route logic changes, replay a fixture matrix against both versions and review changed destinations. A route refactor can alter the entire trajectory even when every node implementation remains unchanged.

Design convergence as carefully as branching. Parallel routes need a named join, compatible reducers, and a policy for partial failure or missing results. A branch that never reports completion can leave the join waiting or produce an incomplete final state that looks successful.

  • Return only named route outcomes and provide a safe fallback.
  • Keep side effects out of route functions.
  • Use reducers for fan-out results that converge in one superstep.
  • Test success, uncertainty, missing data, repeated failure, and loop exhaustion.

Conditional Route Examples

Beginner Example: Route by Ticket Category

A classic branching example: classify first, then send work down a specialized path.

Beginner Example: Route by Ticket Category
from typing_extensions import TypedDict, Literal
from langgraph.graph import StateGraph, START, END

class TicketState(TypedDict):
    message: str
    category: str
    reply: str

def classify(state: TicketState) -> dict:
    text = state["message"].lower()
    if "refund" in text:
        return {"category": "billing"}
    if "error" in text:
        return {"category": "technical"}
    return {"category": "general"}

def route(state: TicketState) -> Literal["billing", "technical", "general"]:
    return state["category"]
  • Keep the classification result in state so the route is transparent.
  • The router returns a constrained set of node names.
  • This is the simplest useful branch pattern in LangGraph.

Intermediate Example: Add a Retry Loop

This shows the minimum safe shape for repetition: a counter, a quality signal, and a fallback.

Intermediate Example: Add a Retry Loop
from typing_extensions import TypedDict, Literal

class ResearchState(TypedDict):
    question: str
    attempts: int
    quality: str

def evaluate(state: ResearchState) -> dict:
    next_attempts = state["attempts"] + 1
    quality = "good" if next_attempts >= 2 else "weak"
    return {"attempts": next_attempts, "quality": quality}

def route_after_evaluate(state: ResearchState) -> Literal["search", "human_review", "__end__"]:
    if state["quality"] == "good":
        return "__end__"
    if state["attempts"] >= 3:
        return "human_review"
    return "search"
  • The loop is bounded by `attempts`.
  • A failed search path still has a safe destination.
  • This shape generalizes to extraction, coding, and QA workflows.

Advanced Example: Decide and Route With Command

This pattern is useful when a node computes a control label and should immediately route without a separate router function.

Advanced Example: Decide and Route With Command
from typing_extensions import TypedDict, Literal
from langgraph.types import Command

class ModerationState(TypedDict):
    text: str
    risk: str

def inspect_text(state: ModerationState) -> Command[Literal["queue_manual_review", "publish"]]:
    flagged = "password" in state["text"].lower()
    if flagged:
        return Command(update={"risk": "high"}, goto="queue_manual_review")
    return Command(update={"risk": "low"}, goto="publish")
  • The node owns both the risk update and the next-hop decision.
  • This keeps the graph tight when the route is inseparable from the node logic.
  • Use it sparingly; a separate router is still often clearer for bigger branch tables.
Before you move on

LangGraph Conditional Workflows: Routing, Loops, and Controlled Branching Mastery Check

4 checks
  • Keep routers short and deterministic where possible.
  • Every loop needs explicit exit and exhaustion behavior.
  • Name branches so the graph reads like a business process.
  • Choose between route functions and `Command` based on readability.

LangGraph Questions Learners Ask

Yes, but deterministic logic is easier to test. Use model-assisted routing only when language understanding is truly part of the decision.

Usually in the node that completes one iteration or in an evaluation node that decides whether another pass is warranted.

A human review node or a structured failure summary, depending on the risk and business impact.

Next Step
Next Practice

Finish the concept here, then reinforce it with hands-on coding, interview prep, or a tool that matches the topic.

Browse Free Tutorials

Explore 500+ free tutorials across 20+ languages and frameworks.