LangGraph Tutorial (Python): building conditional routing for intermediate developers
This tutorial shows you how to build a LangGraph workflow in Python that routes requests conditionally based on the content of the input. You need this when one agent flow is not enough and you want different paths for questions, classification, or fallback handling without writing a pile of if/else code.
What You'll Need
- •Python 3.10+
- •
langgraph - •
langchain-core - •
typing_extensions - •No API key is required for this example because we’ll use deterministic routing logic
- •Basic familiarity with LangGraph nodes, edges, and state
Step-by-Step
- •
Define a typed state and the router decision.
The graph needs a shared state object, plus a routing function that decides which branch to take. Here we route based on whether the input looks like a billing question, a support question, or something else.
from typing import Literal
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END
class RouteState(TypedDict):
query: str
route: str
answer: str
def classify_route(state: RouteState) -> Literal["billing", "support", "fallback"]:
text = state["query"].lower()
if any(word in text for word in ["invoice", "bill", "payment", "charge"]):
return "billing"
if any(word in text for word in ["error", "bug", "issue", "broken"]):
return "support"
return "fallback"
- •
Create one node per branch.
Each node should do one job and write back to the shared state. In production, these nodes often call tools, databases, or LLMs, but here we keep them deterministic so the routing logic is easy to test.
def billing_node(state: RouteState) -> dict:
return {
"route": "billing",
"answer": f"Billing team should handle: {state['query']}"
}
def support_node(state: RouteState) -> dict:
return {
"route": "support",
"answer": f"Support team should handle: {state['query']}"
}
def fallback_node(state: RouteState) -> dict:
return {
"route": "fallback",
"answer": f"General queue should handle: {state['query']}"
}
- •
Build the graph and add conditional edges.
This is the core pattern. The router function returns a label, and
add_conditional_edgesmaps that label to the next node.
graph = StateGraph(RouteState)
graph.add_node("billing", billing_node)
graph.add_node("support", support_node)
graph.add_node("fallback", fallback_node)
graph.add_conditional_edges(
START,
classify_route,
{
"billing": "billing",
"support": "support",
"fallback": "fallback",
},
)
graph.add_edge("billing", END)
graph.add_edge("support", END)
graph.add_edge("fallback", END)
app = graph.compile()
- •
Run the graph with sample inputs.
Use
invoke()with an initial state containing at least the query field. The output will include both the selected route and the final answer from the chosen branch.
if __name__ == "__main__":
samples = [
{"query": "I have a charge on my invoice I don't recognize.", "route": "", "answer": ""},
{"query": "My app has an error when I log in.", "route": "", "answer": ""},
{"query": "Can someone help me understand your service?", "route": "", "answer": ""},
]
for sample in samples:
result = app.invoke(sample)
print("\nINPUT:", sample["query"])
print("ROUTE:", result["route"])
print("ANSWER:", result["answer"])
- •
Add a cleaner routing pattern for real projects.
In intermediate codebases, you usually want your router to be separate from business nodes and return explicit labels only. That keeps branching logic testable and makes it easy to swap rule-based routing for LLM-based classification later.
def route_decision(state: RouteState) -> Literal["billing", "support", "__end__"]:
text = state["query"].lower()
if any(word in text for word in ["invoice", "bill", "payment", "charge"]):
return "billing"
if any(word in text for word in ["error", "bug", "issue", "broken"]):
return "__end__"
return "__end__"
Testing It
Run the script and confirm each sample lands on the expected branch. Billing-related text should set route to "billing", error-related text should go to "support" if you wire it that way, and everything else should fall back cleanly.
If you want stronger verification, add assertions around result["route"] instead of just printing output. That gives you a fast regression check when you change routing rules later.
A good next test is edge cases: empty strings, mixed-intent queries, or inputs containing multiple trigger words. That’s where conditional routing bugs usually show up first.
Next Steps
- •Replace rule-based routing with an LLM classifier node that returns structured labels
- •Add parallel branches for retrieval, summarization, or escalation handling
- •Persist graph state with checkpoints so routed conversations can resume after interruptions
Keep learning
- •The complete AI Agents Roadmap — my full 8-step breakdown
- •Free: The AI Agent Starter Kit — PDF checklist + starter code
- •Work with me — I build AI for banks and insurance companies
By Cyprian Aarons, AI Consultant at Topiax.
Want the complete 8-step roadmap?
Grab the free AI Agent Starter Kit — architecture templates, compliance checklists, and a 7-email deep-dive course.
Get the Starter Kit