Domain Routing¶
Hierarchical domain routing organizes agents and workflows into logical groups, each with its own routing boundary. This is useful for large systems where a flat list of agents becomes unwieldy.
How It Works¶
Domain routing uses a two-tier architecture:
User Message
|
v
Top-Level Router -- cheap/fast LLM classifies into domain
|
+---> "content" -- DomainRouter delegates to content domain
| |
| v
| Content Router -- domain-specific LLM picks agent/workflow
| |
| +---> content_researcher
| +---> content_formatter
| +---> content-creation workflow
|
+---> "support" -- DomainRouter delegates to support domain
| |
| v
| Support Router
| +---> ticket_handler
| +---> faq_agent
|
+---> "direct" -- bypass domain routing, use default agent
Step 1: Define Domains¶
Create domain files in context/domains/:
context/domains/content.domain.md:
---
name: content
description: "Content research, creation, editing, and publishing"
routerModel: claude-sonnet-4-6
routerTemperature: 0.0
agents:
- content_researcher
- content_formatter
workflows:
- content-research
- content-creation
contextFiles:
- shared/content-guidelines.context.md
fallback: content_researcher
---
Content domain: handles all content-related requests including
research, writing, editing, and publishing workflows.
context/domains/support.domain.md:
---
name: support
description: "Customer support, FAQ, and ticket management"
routerModel: claude-sonnet-4-6
routerTemperature: 0.0
agents:
- ticket_handler
- faq_agent
workflows:
- escalation-workflow
fallback: faq_agent
---
Support domain: handles customer inquiries, FAQ responses,
and support ticket management.
Domain Config Fields¶
| Field | Type | Default | Description |
|---|---|---|---|
name |
str |
required | Domain identifier |
description |
str |
"" |
What this domain handles (used by the router LLM) |
routerModel |
str |
"claude-sonnet-4-6" |
LLM model for intra-domain routing |
routerTemperature |
float |
0.0 |
Temperature for routing (low = deterministic) |
agents |
list[str] |
[] |
Agents belonging to this domain |
workflows |
list[str] |
[] |
Workflows belonging to this domain |
contextFiles |
list[str] |
[] |
Shared context for all agents in the domain |
fallback |
str |
"" |
Default agent when intra-domain routing is ambiguous |
Step 2: Configure the Top-Level Router¶
The top-level router's targets are domain names (plus "direct" for pass-through):
context/router.prompt.md:
---
name: main_router
routing_rules:
- if: "'content' in message or 'write' in message or 'blog' in message"
routeTo: content
- if: "'support' in message or 'help' in message or 'ticket' in message"
routeTo: support
fallback: direct
llmFallback: true
---
Classify the user's message into one of these categories:
- content: Content creation, research, editing, or publishing
- support: Customer support, FAQ, or ticket management
- direct: General requests that don't fit a specific domain
Respond with just the category name.
Step 3: Set Up the DomainRouter¶
import asyncio
from agentflow import (
ConfigLoader,
DomainRouter,
RouterEngine,
AnthropicProvider,
EventBus,
)
async def main():
# Load configs (including domains)
loader = ConfigLoader("./context")
loader.load()
events = EventBus()
# Create a factory that produces LLM providers for different models
def llm_factory(model: str) -> AnthropicProvider:
return AnthropicProvider(model=model)
# Set up top-level router
router_config, router_prompt = loader.router
domain_names = list(loader.domains.keys()) + ["direct"]
top_router = RouterEngine(
config=router_config,
router_prompt=router_prompt,
available_targets=domain_names,
llm=AnthropicProvider(),
event_bus=events,
)
# Create domain router
domain_router = DomainRouter(
top_router=top_router,
loader=loader,
llm_factory=llm_factory,
direct_target="general_assistant",
event_bus=events,
)
# Route messages
result = await domain_router.route("Write a blog post about AI safety")
print(f"Target: {result.target}") # "content_researcher" or similar
print(f"Domain: {result.domain}") # "content"
print(f"Method: {result.method}") # "domain:content"
result = await domain_router.route("I need help with my order")
print(f"Target: {result.target}") # "faq_agent" or "ticket_handler"
print(f"Domain: {result.domain}") # "support"
asyncio.run(main())
How DomainRouter Works¶
-
The top-level
RouterEngineclassifies the message into a domain name (e.g.,"content") or"direct". -
If
"direct"(or the static fallback), theDomainRouterreturns immediately with thedirect_targetagent. -
Otherwise, it looks up the
DomainConfigfor that domain, creates (and caches) a domain-levelRouterEngineusing the domain'srouterModel, and routes to a specific agent or workflow within the domain. -
The
RoutingResultincludes adomainfield so callers can track which domain handled the request.
LLM Factory¶
The llm_factory parameter is a callable that creates an LLMProvider for a given model name. Each domain can specify its own routerModel, and the factory creates the appropriate provider:
def llm_factory(model: str) -> LLMProvider:
# Could use different providers for different models
if "gemini" in model:
return GoogleGenAIProvider(model=model)
return AnthropicProvider(model=model)
Events¶
Domain routing emits DOMAIN_ROUTED events:
from agentflow import EventBus, DOMAIN_ROUTED
events = EventBus()
class DomainTracker:
async def on_event(self, event_type: str, data: dict) -> None:
print(f"Domain: {data.get('domain')}")
print(f"Target: {data.get('target')}")
print(f"Method: {data.get('method')}")
events.on(DOMAIN_ROUTED, DomainTracker())
When to Use Domain Routing¶
Use domains when:
- You have 10+ agents and flat routing becomes unclear
- Different areas of your system need different routing models or temperatures
- You want to scope memory, context, and telemetry by domain
- Teams own different domains independently
Use flat routing when:
- You have a small number of agents (< 10)
- Simple YAML rules are sufficient
- You want minimal latency (domain routing adds one LLM call)