Message Routing: Bindings Rule Engine and Eight-Level Priority Matching
Chapter 10: Message Routing: The Bindings Rule Engine and Eight-Level Priority Matching Algorithm
10.1 Why Deterministic Routing Is Necessary
10.1.1 The Chaos Problem in Multi-Agent Multi-Channel Scenarios
Imagine a typical enterprise OpenClaw deployment:
Channels:
Discord server "Engineering"
├── #general (general discussion)
├── #code-review (code review requests)
├── #ops-alerts (operations alerts)
└── Direct Messages (DM)
Agents:
agent_code_reviewer - Specialized in code review
agent_ops_assistant - Specialized in operations issues
agent_general - General-purpose conversational assistant
agent_vip - VIP user exclusive assistant (better model)
Without deterministic routing, any message might be handled by any Agent:
- Messages in #code-review handled by agent_general (functional mismatch)
- VIP users' DMs handled by agent_general (degraded experience)
- Operations alert messages handled by agent_code_reviewer (wrong context)
The more serious problem: if routing rules are ambiguous, the same message might be processed by multiple Agents simultaneously, causing duplicate responses and state conflicts.
10.1.2 Three Guarantees of Deterministic Routing
OpenClaw's Bindings routing engine provides three core guarantees:
- Uniqueness: Each message routes to exactly one Agent (even if multiple rules match, only the highest-priority one is selected)
- Determinism: The same message with the same configuration always routes to the same Agent
- Predictability: The routing result for any message can be predicted by examining the configuration, without observing runtime behavior
10.2 The Eight-Level Priority Matching Algorithm
Bindings priorities are arranged from high to low. Evaluation starts at Level 1 and descends; matching stops as soon as a match is found:
Priority levels (highest to lowest):
Level 1: Peer match (exact DM / group ID match)
Level 2: Parent peer match (thread inherits parent message routing)
Level 3: Guild ID + role (role-based match within a Discord server)
Level 4: Guild ID (Discord server-level match)
Level 5: Team ID (team-level match)
Level 6: Account ID (account-level match)
Level 7: Channel level (channel type match)
Level 8: Default Agent fallback (catch-all routing)
10.2.1 Level 1: Peer Match (Exact ID Match)
Matching logic: The message's source party (peer) exactly matches the peer ID configured in the Binding.
Definition of peer:
- Discord DM: sender's user ID (
user:123456789) - Discord channel message: channel ID (
channel:987654321) - Slack DM: sender's user ID
- API call: explicitly specified peer ID
Configuration example:
bindings:
- name: "VIP Alice - Claude Opus"
match:
peer: "user:alice_discord_id_123" # Exact match of Alice's Discord ID
agent: agent_vip
priority: 1 # Explicitly annotated, ensures it comes first
- name: "Code Review Channel"
match:
peer: "channel:code_review_channel_id" # Exact match of the code review channel
agent: agent_code_reviewer
Use cases:
- Configure a dedicated Agent and model for specific VIP users
- Configure specialized Agents for specific channels (code review, customer service, etc.)
- Configure special handling logic for specific bot accounts
10.2.2 Level 2: Parent Peer Match (Thread Inheritance Mechanism)
Matching logic: The message is a thread reply to an already-routed message, and inherits the parent message's routing result.
Why is thread inheritance needed?
Consider the following scenario:
Discord conversation:
[Original message] Alice → #general: "Help me review this code"
└─ Routed to agent_code_reviewer
[Thread reply] Bob → #general (replying to Alice's message): "I'd also like it to look at mine"
└─ Where should this route?
Without thread inheritance: Bob's message goes through general matching and might route to agent_general, rather than the agent_code_reviewer already handling this code review conversation.
With thread inheritance: Bob's message recognizes that it is a reply to Alice's message, inherits Alice's message's routing result, and also routes to agent_code_reviewer. This preserves conversational context consistency within the same thread.
Implementation mechanism:
function resolveParentPeerMatch(message: IncomingMessage): Binding | null {
// Find the parent message ID
const parentMessageId = message.replyToId || message.threadParentId;
if (!parentMessageId) return null;
// Look up the parent message's routing result
const parentRouting = routingCache.get(parentMessageId);
if (!parentRouting) return null;
// Inherit the parent message's Binding
return parentRouting.binding;
}
Configuration: Thread inheritance is the default behavior, no explicit configuration needed. To disable it:
bindings:
- name: "General Channel"
match:
peer: "channel:general_id"
agent: agent_general
threadInheritance: false # Disable thread inheritance
10.2.3 Level 3: Guild ID + Role
Matching logic: The message comes from a specific Discord server (Guild) and the sender holds a specific role.
Configuration example:
bindings:
- name: "Engineering Team - Code Agent"
match:
guildId: "guild:my_company_discord"
roles:
- "role:engineer"
- "role:senior_engineer"
- "role:tech_lead"
agent: agent_code_reviewer
- name: "Management - Executive Agent"
match:
guildId: "guild:my_company_discord"
roles:
- "role:manager"
- "role:director"
- "role:vp"
agent: agent_executive_assistant
Role matching strategy:
rolesMatchMode: "any" # Match if the sender has any one of the listed roles (default)
rolesMatchMode: "all" # Match only if the sender has all listed roles
Discord use cases: This is the most important routing mechanism for OpenClaw in the Discord ecosystem. An enterprise Discord server can achieve fine-grained Agent segregation through roles:
Discord server "My Company"
role: engineer → agent_code
role: sales → agent_crm
role: support → agent_helpdesk
role: @everyone → agent_general (fallback via Level 4)
10.2.4 Level 4: Guild ID (Server-Level)
Matching logic: The message comes from a specific Discord server, without role restrictions.
bindings:
- name: "Partner Company Discord"
match:
guildId: "guild:partner_company_discord"
agent: agent_partner_support
- name: "Internal Company Discord"
match:
guildId: "guild:my_company_discord"
agent: agent_internal_general
Typical uses:
- Different Discord servers map to different Agent instances
- Partner community vs. internal community routes to different Agents
- Multi-tenant deployments: each tenant has an independent Discord server
10.2.5 Level 5: Team ID (Team-Level)
Matching logic: The message source belongs to a specific Team (an organizational unit in OpenClaw that spans channels).
bindings:
- name: "Frontend Team"
match:
teamId: "team:frontend"
agent: agent_frontend
- name: "Backend Team"
match:
teamId: "team:backend"
agent: agent_backend
The concept of Team: A Team in OpenClaw is a cross-channel organizational unit. A user can belong to multiple Teams, and Team members can come from different channels (Discord, Slack, API).
# config/teams.yaml
teams:
- id: team:frontend
name: "Frontend Engineering"
members:
- { type: "discord-user", id: "user:alice" }
- { type: "slack-user", id: "U01234567" }
- { type: "email", id: "[email protected]" }
10.2.6 Level 6: Account ID (Account-Level)
Matching logic: The message source belongs to a specific account (a unified account after cross-channel identity linking).
bindings:
- name: "Enterprise Customer Alice"
match:
accountId: "account:enterprise_alice"
agent: agent_enterprise
model: "claude-opus-4-5" # Better model
- name: "Free Tier Users"
match:
accountId: "account:free_*" # Wildcards supported
agent: agent_free
model: "claude-3-5-haiku" # Economy model
Relationship between Account and channel identity:
Unified Account
├── Discord: user:123
├── Slack: U04567
└── Email: [email protected]
A user may contact OpenClaw through multiple channels; the Account ID unifies these identities to enable consistent cross-channel routing policies.
10.2.7 Level 7: Channel Level (Channel Type Match)
Matching logic: Match based on the message's channel type (not a specific ID).
bindings:
- name: "All Discord DMs"
match:
channel:
type: "discord-dm"
agent: agent_dm_assistant
- name: "All Slack DMs"
match:
channel:
type: "slack-dm"
agent: agent_slack_assistant
- name: "All API Calls"
match:
channel:
type: "api"
agent: agent_api
Common channel types:
discord-dm - Discord direct messages
discord-channel - Discord channel messages
discord-thread - Discord thread messages
slack-dm - Slack direct messages
slack-channel - Slack channel messages
slack-thread - Slack thread messages
api - REST/WebSocket API calls
webhook - External Webhook triggers
10.2.8 Level 8: Default Agent Fallback
Matching logic: The catch-all routing used when none of the first 7 levels match.
bindings:
- name: "Default Fallback"
match: "*" # Matches everything
agent: agent_general
isDefault: true # Mark as default fallback
If no default Agent is configured:
{
"type": "res",
"ok": false,
"error": {
"code": "NO_ROUTE_FOUND",
"message": "No binding matched the incoming message and no default agent is configured",
"details": {
"messageId": "msg_001",
"source": { "channelType": "discord-dm", "userId": "user:unknown" }
}
}
}
Best practice: Always configure a default fallback to avoid routing failures that show error messages to users.
10.3 Complete Routing Match Flow
Incoming message received
│
▼
Extract message attributes:
peer, parentId, guildId, roles[], teamId, accountId, channelType
│
▼
Level 1: Peer Match
│
├── Matching Binding found ─────────────────→ Route to Agent, done
│
└── Not found
│
▼
Level 2: Parent Peer Match
│
├── Has parent msg & parent has routing record ──→ Inherit routing, done
│
└── No parent msg or no routing record
│
▼
Level 3: Guild ID + Roles
│
├── Match found (guildId AND role match) ──────→ Route to Agent, done
│
└── Not found
│
▼
Level 4: Guild ID Only
│
├── Match found (guildId match only) ──────────→ Route to Agent, done
│
└── Not found
│
▼
Level 5: Team ID
│
├── Match found ────────────────────────────→ Route to Agent, done
│
└── Not found
│
▼
Level 6: Account ID
│
├── Match found ────────────────────────────→ Route to Agent, done
│
└── Not found
│
▼
Level 7: Channel Type
│
├── Match found ────────────────────────────→ Route to Agent, done
│
└── Not found
│
▼
Level 8: Default Fallback
│
├── Default Agent configured ─────────────→ Route to default Agent, done
│
└── No default Agent ──────────────────────→ Return NO_ROUTE_FOUND error
10.4 Bindings Configuration Examples
10.4.1 Channel-Based Traffic Segregation
# config/bindings.yaml
bindings:
# Discord code review channel → Code review Agent
- name: "discord-code-review"
match:
peer: "channel:${DISCORD_CODE_REVIEW_CHANNEL_ID}"
agent: agent_code_reviewer
responseMode: "thread" # Reply in threads to keep the channel clean
# Discord ops alert channel → Ops Agent (quiet mode, only replies when necessary)
- name: "discord-ops-alerts"
match:
peer: "channel:${DISCORD_OPS_CHANNEL_ID}"
agent: agent_ops
responseMode: "reaction-only" # Acknowledge with emoji reactions only
# All Discord DMs → General assistant
- name: "discord-dm-general"
match:
channel:
type: "discord-dm"
agent: agent_general
# API calls → Stateless API Agent
- name: "api-requests"
match:
channel:
type: "api"
agent: agent_api
sessionMode: "ephemeral" # Do not persist session
10.4.2 Account-Based Traffic Segregation (Tiered Service)
bindings:
# Enterprise accounts → Premium Agent + best model
- name: "enterprise-tier"
match:
accountId: "account:enterprise_*"
agent: agent_enterprise
model: "claude-opus-4-5"
rateLimit:
requestsPerHour: 500
# Professional accounts → Standard Agent
- name: "pro-tier"
match:
accountId: "account:pro_*"
agent: agent_pro
model: "claude-3-5-sonnet"
rateLimit:
requestsPerHour: 100
# Free accounts → Lightweight Agent + speed-optimized model
- name: "free-tier"
match:
accountId: "account:free_*"
agent: agent_free
model: "claude-3-5-haiku"
rateLimit:
requestsPerHour: 20
# Default fallback
- name: "default"
match: "*"
agent: agent_general
model: "claude-3-5-haiku"
10.4.3 Multi-Agent Discord Scenario
bindings:
# CTO exclusive (highest priority)
- name: "cto-personal"
match:
peer: "user:${CTO_DISCORD_ID}"
agent: agent_executive
model: "claude-opus-4-5"
# Engineering role (Level 3: guildId + role)
- name: "engineers-code-agent"
match:
guildId: "guild:${COMPANY_DISCORD}"
roles: ["role:engineer", "role:senior-engineer"]
agent: agent_code
# Product role
- name: "product-team"
match:
guildId: "guild:${COMPANY_DISCORD}"
roles: ["role:product-manager", "role:designer"]
agent: agent_product
# All company Discord members (Level 4: guildId only)
- name: "company-general"
match:
guildId: "guild:${COMPANY_DISCORD}"
agent: agent_company_general
# Public Discord server
- name: "public-discord"
match:
guildId: "guild:${PUBLIC_COMMUNITY_DISCORD}"
agent: agent_community
# Default fallback
- name: "default"
match: "*"
agent: agent_general
10.5 Session Freshness Evaluation
After routing to an Agent, the system must decide whether to use an existing Session or create a new one. This is determined by Session Freshness evaluation.
10.5.1 Freshness Evaluation Criteria
function isSessionFresh(session: Session, config: FreshnessConfig): boolean {
const now = Date.now();
// Criterion 1: Idle timeout
const idleMs = now - session.lastActiveAt;
if (idleMs > config.idleTimeoutMs) {
return false; // Session has timed out
}
// Criterion 2: Daily reset (default at 4 AM)
const sessionDate = new Date(session.lastActiveAt);
const resetHour = config.dailyResetHour ?? 4; // Default 4 AM
// Determine if the reset time point has passed since the last activity
const sessionDateResetTime = new Date(sessionDate);
sessionDateResetTime.setHours(resetHour, 0, 0, 0);
if (sessionDateResetTime < sessionDate) {
// Reset time was yesterday, advance by one day
sessionDateResetTime.setDate(sessionDateResetTime.getDate() + 1);
}
if (now > sessionDateResetTime.getTime()) {
return false; // The daily reset time has already passed
}
return true; // Session is still fresh
}
10.5.2 Freshness Configuration
# config/sessions.yaml
sessionFreshness:
idleTimeoutMs: 3600000 # 1 hour idle timeout (milliseconds)
dailyResetHour: 4 # Reset at 4 AM every day
# Per-Agent overrides
agentOverrides:
agent_code:
idleTimeoutMs: 7200000 # Code tasks allow 2 hours of idle time
agent_general:
idleTimeoutMs: 1800000 # General conversation: 30-minute idle timeout
10.5.3 Handling After Freshness Expires
Session Freshness evaluation:
Does an existing Session exist?
│
Yes ──→ Is the Session fresh?
│ │
│ Yes ──→ Reuse existing Session (with historical context)
│ │
│ No ──→ Archive old Session, create new Session
│
No ──→ Create new Session
Archived sessions are retained in data/transcripts/, and users can browse historical sessions through the TUI.
10.6 Handling Routing Failures
10.6.1 No Matching Rule
{
"type": "event",
"event": "session.error",
"payload": {
"code": "NO_ROUTE_FOUND",
"message": "No binding matched the incoming message",
"suggestions": [
"Check if a default binding is configured",
"Verify the channel ID is correct in your bindings config"
]
}
}
10.6.2 Agent Unavailable
{
"type": "event",
"event": "session.error",
"payload": {
"code": "AGENT_UNAVAILABLE",
"message": "The matched agent 'agent_code' is not running",
"fallbackAgent": "agent_general",
"usedFallback": true
}
}
10.6.3 Routing Debugging Tools
# Simulate routing decisions (without actually sending messages)
openclaw route simulate \
--peer "user:alice_discord_123" \
--guild "guild:company_discord" \
--roles "engineer,senior-engineer" \
--channel-type "discord-dm"
# Output:
# Level 1 (Peer Match): agent_vip ← Matched! Stop evaluation
# Final Route: agent_vip
# Session: sess_01HXYZ (fresh, last active 5 minutes ago)
# View match statistics for all Bindings
openclaw bindings stats --period 24h
10.7 Advanced Features of Bindings
10.7.1 Conditional Routing (Context-based Routing)
bindings:
- name: "Code Review - Working Hours"
match:
peer: "channel:code_review_id"
conditions:
# Route to professional Agent only during working hours (9 AM - 6 PM)
- timeRange:
start: "09:00"
end: "18:00"
timezone: "Asia/Shanghai"
agent: agent_code_reviewer
- name: "Code Review - After Hours"
match:
peer: "channel:code_review_id"
# After hours, route to lightweight model
agent: agent_code_reviewer_lite
model: "claude-3-5-haiku"
10.7.2 Load-Balanced Routing
bindings:
- name: "Load Balanced Agents"
match:
channel:
type: "api"
agents:
- agent: agent_api_1
weight: 50
- agent: agent_api_2
weight: 50
loadBalancing: "weighted-random"
10.7.3 Numeric Priority Within Levels
When multiple Bindings at the same Level match simultaneously, the priority numeric value determines the final selection:
bindings:
- name: "Alice VIP"
match:
peer: "user:alice"
agent: agent_vip
priority: 100 # Higher value means higher priority (within the same Level)
- name: "Alice Normal"
match:
peer: "user:alice"
agent: agent_general
priority: 10 # This rule will never be selected (Alice matches priority:100 first)
Chapter Summary
The Bindings rule engine is the foundation of OpenClaw's multi-Agent orchestration capability:
- Deterministic routing guarantees each message routes to exactly one Agent, eliminating multi-Agent competition and duplicate responses
- Eight-level priority algorithm descends from exact to fuzzy matching, covering all possible routing scenarios
- Level 1 (Peer Match) handles precise routing for VIP users and specific channels
- Level 2 (Parent Peer Match) preserves conversational context consistency through thread inheritance
- Level 3 (Guild ID + Role) is the most important routing dimension in Discord deployments
- Levels 4-6 (Guild/Team/Account) provide layered routing from server to team to individual
- Level 7 (Channel type) segregates traffic by channel form (DM/channel/API)
- Level 8 (Default fallback) catches all unmatched messages to prevent routing failures
- Session Freshness determines whether to reuse an existing session or create a new one, via idle timeout and daily reset
- Routing failure handling includes no-match errors, Agent unavailability fallback, and debugging tool support
With this, Chapters 6 through 10 of the Complete Guide to OpenClaw have fully presented the complete technical chain spanning the Gateway control plane, Command Queue, Pi framework, execution loop, and message routing. These five chapters form the core knowledge backbone for understanding OpenClaw's architecture.