AI Engineering Primer: Routing - Agentic Workflow Design Pattern
A comprehensive guide to understanding and implementing the Routing Design Pattern within agentic workflows using LangChain and LangGraph.
1. Introduction to the Routing Design Pattern
In modern AI agent architecture, complex workflows cannot always be solved using a single monolithic prompt or a static linear sequence of steps. When an application must handle distinct categories of requests—each requiring specialized context, tailored prompts, or unique execution parameters—the Routing Design Pattern serves as an essential architectural component.
Routing is an agentic design pattern where an input query is dynamically classified by a decision-making node (the Router) and directed to a specialized downstream processing node.
Core Benefits of Routing
- Separation of Concerns: Logic, prompt context, and execution details are isolated into distinct nodes rather than combined into a fragile single prompt.
- Specialized System Prompts: Downstream nodes maintain focused prompts dedicated exclusively to their target task (e.g., extraction rules specific to GitHub vs. Medium).
- Fault Tolerance and Fallback: Requests that fall outside supported capabilities can be cleanly identified and routed to termination or fallback handlers without executing unnecessary downstream operations.
2. Core Mental Model and Operational Flow
The core mental model behind routing relies on intent classification followed by conditional branching.
Real-Life Analogy
Imagine a customer entering a departmental support desk. Instead of a single representative attempting to solve technical issues, process billing refunds, and arrange product shipments all at once, a dedicated receptionist (the Router) assesses the customer's request. The receptionist routes technical questions to the IT Desk, billing inquiries to Finance, and unsupported requests to an Exit / Escalation Desk.
Operational Stages of a Router Node
- Context Ingestion: The router receives the raw user input alongside any shared application state.
- Intent Parsing & Extraction: Using structured LLM output decoding, the router classifies the user's intent and extracts required parameters (e.g., profile handles, search keywords).
- Branching Decision: A conditional edge evaluates the classification output and passes execution control to the appropriate branch.
3. Architecture of the Social Media Profile Summarizer
To demonstrate the routing pattern in practice, we build an agentic workflow in LangGraph that ingests a user prompt requesting a social media profile summary. The agent determines whether the input target is a GitHub profile or a Medium profile, extracts the corresponding handle, fetches profile content, and generates a structured summary. If the platform is unsupported (e.g., LinkedIn), the router terminates the workflow safely.
Complete Workflow Architecture
4. Defining State and Output Schemas
In LangGraph, communication between nodes is strictly managed via a Shared State dictionary structure. To guarantee that the router node outputs deterministic classification results and parameters, we enforce a structured Pydantic output schema.
4.1 Router Output Schema (RouterOutput)
The router must return two critical items:
output: A constrained string literal specifying the target downstream destination ("GitHub","Medium", or"none").user_handle: The profile handle extracted from the user query.
from typing import Literal
from pydantic import BaseModel, Field
class RouterOutput(BaseModel):
"""Structured output schema for the Router Agent decision."""
output: Literal["GitHub", "Medium", "none"] = Field(
description="The target destination node: 'GitHub' or 'Medium'. If neither is supported, use 'none'."
)
user_handle: str = Field(
description="The extracted user profile handle corresponding to the destination."
)
4.2 Shared Graph State Schema (SharedState)
The global state schema holds parameters across the lifecycle of the workflow execution graph.
from typing import TypedDict, Optional
from langchain_openai import ChatOpenAI
class SharedState(TypedDict):
query: str # Raw input query from the user
user_handle: Optional[str] # Handle extracted by the router
destination: Optional[str] # Selected target node string
profile_summary: Optional[str] # Generated markdown profile summary
model: Optional[ChatOpenAI] # Instantiated LLM reference
5. Implementing the Router Agent and Decision Engine
The system uses two initial nodes: build_model initializes the core LLM instance into the shared state, and router_agent applies a structured system prompt to make the routing decision.
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
def build_model(state: SharedState) -> SharedState:
"""Node: Instantiates the LLM instance and stores it in the shared state."""
llm = ChatOpenAI(model="gpt-4o", temperature=0)
return {"model": llm}
def router_agent(state: SharedState) -> SharedState:
"""
Node: Analyzes user query, classifies destination, and extracts user handle.
Enforces output compliance with RouterOutput schema.
"""
model = state["model"]
# Enforce structured output parsing matching Pydantic schema
structured_model = model.with_structured_output(RouterOutput)
prompt = ChatPromptTemplate.from_messages([
("system", (
"You are an expert social media profile assistant.\n"
"Based on the user query, determine whether to process their GitHub or Medium profile.\n"
"Ensure your response strictly matches the RouterOutput schema."
)),
("human", "{query}")
])
chain = prompt | structured_model
decision: RouterOutput = chain.invoke({"query": state["query"]})
# Write extraction results to shared graph state
return {
"destination": decision.output,
"user_handle": decision.user_handle
}
6. Helper Utilities for Content Extraction
The downstream nodes rely on dedicated content loaders to scrape web page details for the target social media user handle using LangChain's WebBaseLoader.
Note: Profiles on different platforms follow distinct URL naming rules.
- GitHub Pattern:
https://github.com/<user_handle>- Medium Pattern:
https://<user_handle>.medium.com
from langchain_community.document_loaders import WebBaseLoader
def read_github_profile(user_handle: str) -> str:
"""Helper: Fetches raw page content from a GitHub user profile."""
url = f"https://github.com/{user_handle}"
loader = WebBaseLoader(url)
docs = loader.load()
combined_content = "\n".join([doc.page_content for doc in docs])
return combined_content
def read_medium_profile(user_handle: str) -> str:
"""Helper: Fetches raw page content from a Medium user profile."""
url = f"https://{user_handle}.medium.com"
loader = WebBaseLoader(url)
docs = loader.load()
combined_content = "\n".join([doc.page_content for doc in docs])
return combined_content
7. Specialized Downstream Execution Nodes
Each downstream node encapsulates its own system prompt and processing logic tailored strictly to the requirements of the target platform.
7.1 Specialized GitHub Summarizer Node
from langchain_core.prompts import ChatPromptTemplate
def summarize_github_profile(state: SharedState) -> SharedState:
"""Node: Extracts and summarizes GitHub profile content using a platform-specific prompt."""
user_handle = state["user_handle"]
model = state["model"]
# Fetch raw unstructured context
raw_content = read_github_profile(user_handle)
# Specialized prompt localized to GitHub features
prompt = ChatPromptTemplate.from_messages([
("system", (
"You are an expert technical recruiter and open-source analyst.\n"
"Summarize the following GitHub profile content in a concise, structured manner.\n"
"Highlight key details: Name, Organization, Followers, Location, Contact Info, "
"Key Skills, Top Projects, and Open-Source Contributions."
)),
("human", "Profile Content:\n{content}")
])
chain = prompt | model
response = chain.invoke({"content": raw_content})
return {"profile_summary": response.content}
7.2 Specialized Medium Summarizer Node
def summarize_medium_profile(state: SharedState) -> SharedState:
"""Node: Extracts and summarizes Medium profile content using a writing-centric prompt."""
user_handle = state["user_handle"]
model = state["model"]
# Fetch raw unstructured context
raw_content = read_medium_profile(user_handle)
# Specialized prompt localized to Medium publication metrics
prompt = ChatPromptTemplate.from_messages([
("system", (
"You are an expert editorial analyst.\n"
"Summarize the following Medium profile content in a concise, structured manner.\n"
"Highlight key details: Name, Followers, Posted Articles/Topics, and Profile Description."
)),
("human", "Profile Content:\n{content}")
])
chain = prompt | model
response = chain.invoke({"content": raw_content})
return {"profile_summary": response.content}
8. Graph Assembly and Conditional Routing Logic
With nodes defined, we assemble the state graph in LangGraph. Control flow branching is achieved via a Conditional Edge function (route_destination) that reads the destination variable set by the router_agent.
8.1 Defining the Routing Logic Function
from langgraph.graph import END
def route_destination(state: SharedState) -> str:
"""
Conditional Edge Decision Logic:
Evaluates state['destination'] to determine the target graph node.
"""
dest = state.get("destination")
if dest == "GitHub":
return "summarize_github_profile"
elif dest == "Medium":
return "summarize_medium_profile"
else:
# Fallback for unsupported domains (e.g. LinkedIn)
return END
8.2 Assembling and Compiling the State Graph
from langgraph.graph import StateGraph, START, END
def build_profile_summarizer_graph():
builder = StateGraph(SharedState)
# Register Graph Nodes
builder.add_node("build_model", build_model)
builder.add_node("router_agent", router_agent)
builder.add_node("summarize_github_profile", summarize_github_profile)
builder.add_node("summarize_medium_profile", summarize_medium_profile)
# Establish Standard Edges
builder.add_edge(START, "build_model")
builder.add_edge("build_model", "router_agent")
# Establish Conditional Edge from Router Agent
builder.add_conditional_edges(
"router_agent",
route_destination,
{
"summarize_github_profile": "summarize_github_profile",
"summarize_medium_profile": "summarize_medium_profile",
END: END
}
)
# Terminal Edges from Processing Nodes to Graph Exit
builder.add_edge("summarize_github_profile", END)
builder.add_edge("summarize_medium_profile", END)
return builder.compile()
9. Full Walkthrough: Execution Scenarios & Output Validation
Below are the execution patterns and outputs across three test query scenarios: GitHub, Medium, and an unsupported platform (LinkedIn).
Scenario 1: GitHub Summarization Request
graph = build_profile_summarizer_graph()
input_state = {
"query": "Can you summarize the GitHub profile for handle sorofP97?"
}
result = graph.invoke(input_state)
print(f"Destination Decision: {result['destination']}")
print(f"User Handle: {result['user_handle']}")
print(f"Profile Summary:\n{result['profile_summary']}")
Destination Decision: GitHub
User Handle: sorofP97
Profile Summary:
- Name: Sorov Pratik
- Username: sorofP97
- Organization: Google
- Previous Experience: GeeksforGeeks
- Followers: 171
- Key Skills: AI Engineering, Python, System Design
- Projects: LangChain Agents, LLM Workflows
Scenario 2: Medium Summarization Request
input_state = {
"query": "Please read and summarize the Medium profile for user SRBPTK97."
}
result = graph.invoke(input_state)
print(f"Destination Decision: {result['destination']}")
print(f"User Handle: {result['user_handle']}")
print(f"Profile Summary:\n{result['profile_summary']}")
Destination Decision: Medium
User Handle: SRBPTK97
Profile Summary:
- Name: Sorov Pratik
- Profile Type: Medium Author
- Followers: 250+
- Key Articles Posted:
1. Principles of Clean Coding
2. My Google Interview Experience
3. System Design for AI Developers
Scenario 3: Unsupported Platform Fallback Request
input_state = {
"query": "Summarize the LinkedIn profile for handle sorof-pratik."
}
result = graph.invoke(input_state)
if result.get("destination") == "none" or not result.get("profile_summary"):
print("Execution Result: No suitable profile processor found for this request.")
Destination Decision: none
Execution Result: No suitable profile processor found for this request.
10. Architectural Analysis: Routing vs. Prompt Chaining
To understand when to choose Routing over other workflow patterns like Prompt Chaining, consider the key architectural dimensions summarized below:
Summary and Key Takeaways
- Intent Classification Core: The Routing Pattern classifies user intent upfront to direct control flow dynamically to targeted downstream sub-agents or nodes.
- Enforced Separation of Concerns: Isolating execution nodes keeps system prompts concise, highly targeted, and manageable, avoiding monolithic "do-everything" prompts.
- Structured Schema Enforcers: Router reliability depends on using structured output schemas (such as Pydantic models) to guarantee clean, machine-readable decisions.
- State-Driven Information Passing: Critical metadata extracted by the router (e.g., target domain, user handles) is written to a global graph state and accessed seamlessly by downstream nodes.
- Deterministic Dynamic Branching: Using framework constructs like LangGraph's
add_conditional_edges, applications achieve clean decision trees with predictable outcomes. - Built-in Fault Tolerance: Requests that target unsupported domains are mapped cleanly to explicit exit paths (e.g., returning
"none"and ending execution), preventing downstream system failures and saving API costs.