Router: sending each question to one agent
A router is a classification step that sends each question to one specialist and then steps aside.
Last updated: 27 Sep, 2026 · LangChain 1.4
A router picks the next step
A router is the simplest decision in a multi-agent system: look at the state and return the name of whoever should act next. In the multi-agent crash course, once the researcher, analyst and writer agents exist, the router is a plain function. It takes the supervisor state and reads next_agent, falling back to the supervisor. If next_agent is end, or the task is complete, it returns END. If it names the supervisor, the researcher, the analyst or the writer, it returns that name. Anything else goes back to the supervisor:
def router(state: SupervisorState) -> Literal["supervisor", "researcher", "analyst", "writer", "__end__"]:
"""Routes to next agent based on state"""
next_agent = state.get("next_agent", "supervisor")
if next_agent == "end" or state.get("task_complete", False):
return END
if next_agent in ["supervisor", "researcher", "analyst", "writer"]:
return next_agent
return "supervisor"The workflow then wires it in: a graph over the supervisor state with four nodes (supervisor, researcher, analyst and writer), the supervisor as the entry point, and a conditional edge from each node that calls router and goes wherever it points. Invoked with "What are the benefits and risks of AI in healthcare?", the run went to the researcher, then the analyst, then the writer, and ended with the final report, though the question never reached the agents: graph.invoke was given a bare HumanMessage instead of {"messages": [...]}, so the researcher reported on "No task" and the report is a generic template. Pass the input as a dictionary with a messages list, as every example in this course does.
The return type lists every place the run can go, and END stops it. No model is called in the router; the supervisor already made the choice. The shop's router is a plain function too. The difference is what it reads: the customer's question itself, not a field a supervisor filled in. An order id sends it to orders, and anything else goes to policies. That makes it cheaper and more predictable than the supervisor in Subagents as tools, but it cannot combine answers.
The video's router lives inside a LangGraph graph and reads the name the supervisor wrote into the state. The shop's router below is a plain function too, but it reads the customer's question itself, so no supervisor is needed.
The two specialists come from the subagents lesson: orders_agent looks up orders and policies_agent answers from the policy documents, both real Groq agents in specialists.py. Here a router sends each question to one of them.
The route function
def route(question):
if some_rule(question): # one classification step
return "orders"
return "policies" # the default specialistRouting by an order id
route picks a specialist by looking for an order id and returns its name. Nothing decides here except a regular expression. The code in this section goes in one file, router.py.
import re
def route(question):
if re.search(r"\b[A-Z]\d+\b", question): # an order id, like A17
return "orders"
return "policies" # everything elseThe answer function
answer hands the question to the chosen agent and returns which one replied. It takes the agents as a dictionary instead of importing them, so router.py never builds a model itself; the testing lesson relies on that.
def answer(question, agents):
name = route(question) # pick one specialist
result = agents[name].invoke({"messages": [{"role": "user", "content": question}]})
return name, result["messages"][-1].text # the question went straight through- written in Documents and splitting
- written in Embeddings and a vector store
- written in Retrieval as a tool
- written in Subagents as tools
- written in Subagents as tools
View the code here
from langchain_core.documents import Document
from langchain_text_splitters import RecursiveCharacterTextSplitter
POLICIES = {
"refunds.md": "Refunds go back to the card you paid with. They take up to 5 working days to arrive."
"\n\nYou can ask for a refund within 30 days of delivery. Opened items can be refunded if they are faulty.",
"shipping.md": "Standard shipping takes 3 to 5 working days. Shipping is free on orders over 50 euros."
"\n\nExpress shipping arrives the next working day and costs 9 euros.",
"accounts.md": "To reset your password, use the reset link on the sign-in page. Support staff never ask for your password.",
}
docs = [Document(page_content=text, metadata={"source": name}) for name, text in POLICIES.items()]
splitter = RecursiveCharacterTextSplitter(chunk_size=120, chunk_overlap=0, add_start_index=True)
chunks = splitter.split_documents(docs)
import re
import zlib
from langchain_core.embeddings import Embeddings
COMMON = {"a", "an", "and", "are", "can", "do", "does", "for", "how", "i",
"if", "is", "it", "my", "of", "on", "the", "to", "what", "with", "you", "your"}
class WordEmbeddings(Embeddings):
def embed_query(self, text):
vector = [0.0] * 256
for word in re.findall(r"[a-z]+", text.lower()):
if word not in COMMON:
vector[zlib.crc32(word.rstrip("s").encode()) % 256] += 1.0
return vector
def embed_documents(self, texts):
return [self.embed_query(text) for text in texts]
from langchain.tools import tool
from langchain_core.vectorstores import InMemoryVectorStore
from policies import chunks
from word_embeddings import WordEmbeddings
store = InMemoryVectorStore(WordEmbeddings())
store.add_documents(chunks)
@tool
def search_policies(query: str) -> str:
"""Search the shop's policies on refunds, shipping and accounts.
Pass the customer's question, word for word, as the query."""
found = [doc for doc, score in store.similarity_search_with_score(query, k=2) if score >= 0.3]
if not found:
return "No policy covers this."
return "\n".join(f"[{doc.metadata['source']}] {doc.page_content}" for doc in found)
from langchain.tools import tool
ORDERS = {"A17": "shipped on 3 March", "C40": "waiting for stock"}
@tool
def lookup_order(order_id: str) -> str:
"""Look up an order's shipping status by its id, such as A17."""
status = ORDERS.get(order_id)
return f"{order_id} {status}." if status else f"{order_id} is not an order we have."
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
from langchain.tools import tool
from orders import lookup_order
from search import search_policies
orders_agent = create_agent(init_chat_model("groq:openai/gpt-oss-120b", temperature=0),
tools=[lookup_order], system_prompt="Answer the order question in one short sentence, using only what the lookup_order tool returned.")
policies_agent = create_agent(init_chat_model("groq:openai/gpt-oss-120b", temperature=0),
tools=[search_policies], system_prompt="Answer in one short sentence of plain text, using only what the search_policies tool returned. Name the source file in square brackets, like [refunds.md]. If the tool finds nothing, say the policies do not cover it.")
def last_reply(agent, question):
return agent.invoke({"messages": [{"role": "user", "content": question}]})["messages"][-1].text
@tool
def ask_orders(question: str) -> str:
"""Ask the orders specialist where an order is."""
return last_reply(orders_agent, question)
@tool
def ask_policies(question: str) -> str:
"""Ask the policies specialist about refunds, shipping and accounts."""
return last_reply(policies_agent, question)
Running three questions through the router
Three questions through the router, with the real specialists: two that fit one specialist, and one that needs two.
from router import answer
from specialists import orders_agent, policies_agent
agents = {"orders": orders_agent, "policies": policies_agent}
for question in ["Where is A17?", "Is shipping free?", "Where is A17, and how long does a refund take?"]:
name, reply = answer(question, agents)
print(f"{name:<8} {reply}")orders A17 shipped on 3 March. policies Shipping is free on orders over 50 euros. [shipping.md] orders A17 shipped on 3 March.
Where the router hands off, and where it slips
- The first two questions reached the right agent with no model deciding anything; a rule did it.
- The third shows the limit: it mentions an order, so the whole sentence went to the orders agent alone.
- The orders agent answered the A17 half; the refund half was dropped. The orders agent has no policy tool, so the refund question never reached the policies.
- A router makes one choice: a question that needs two agents needs the supervisor from the subagents lesson, or a router that fans out and merges the answers, which the docs build with LangGraph.
Router vs supervisor
| Router | Supervisor | |
|---|---|---|
| Decision | One step, up front | The model, every turn |
| Cost | One call, or plain rules | A model call per turn |
| Two areas at once | Cannot combine | Asks each and joins |
| Predictable | Same route every time with rules | Depends on the model |
When a router beats a supervisor
- Sending each question to exactly one specialist by its topic.
- A cheap first pass that escalates only the hard cases to a supervisor.
Related
- Previous: Subagents as tools
- Next: Testing an agent
- Reference: Multi-agent
- Route questions that contain "refund" and an order id to the policies agent, and test both kinds.
- Add a third specialist for account questions and a rule for it.
- Print how many model calls
answermakes for one question.
Every expert started right here.