Tutorials Logic, IN info@tutorialslogic.com

LangGraph Conditional Workflows: Routing, Loops, and Controlled Branching

LangGraph Conditional Workflows

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.

Routing Functions Should Be Tiny and Boring

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.

Expert Practice Lab

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.

Final Expert Note

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.

Review Margin

For deeper learning, pair the concept with a tiny graph and a saved trace. Seeing state before and after each node makes the lesson concrete and gives learners a reliable debugging habit from the beginning.

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.
Key Takeaways
  • 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.
Common Mistakes to Avoid
Creating loops with no maximum attempt count.
Letting a model alone decide security-sensitive routes.
Mixing multiple routing mechanisms from one node until control flow becomes ambiguous.

Practice Tasks

  • Add a retry loop to a retrieval graph and stop after two failed evaluations.
  • Write unit tests for each route outcome in a branchy workflow.
  • Refactor one large route function into a scoring node plus simple router.

Frequently Asked Questions

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.

Ready to Level Up Your Skills?

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