Skip to main content
The gateway now ships in the praisonai-bot package. praisonai serve gateway still works exactly as documented here; for a standalone install see praisonai-bot Migration.
from praisonaiagents import Agent

vip_agent = Agent(name="vip-agent", instructions="Handle VIP user requests with priority.")
support_agent = Agent(name="support", instructions="Handle general support queries.")
vip_agent.start("Route messages from user 123 to me, all others to support.")
Route bindings let one gateway send the right message to the right agent — by who sent it, what role they have, which channel it landed in, or which bot account received it. The user messages a channel; route bindings map that channel to the correct agent or workflow.

Quick Start

1

Send everyone to one agent (baseline)

Start with a single default route — no bindings needed.
agents:
  general:
    instructions: "You are a helpful assistant"
    model: gpt-4o-mini

channels:
  telegram:
    token: ${TELEGRAM_BOT_TOKEN}
    routes:
      default: general
2

Send one VIP user to a dedicated agent

Add a bindings: entry with peer: set to the user’s Telegram numeric id. The VIP agent handles that user; everyone else still goes to general.
agents:
  general:
    instructions: "You are a helpful assistant"
    model: gpt-4o-mini
  vip:
    instructions: "You are the VIP concierge."
    model: gpt-4o

channels:
  telegram:
    token: ${TELEGRAM_BOT_TOKEN}
    routes:
      default: general
    bindings:
      - { peer: "12345678", agent: vip }
3

Mix peer, role, channel, and chat-type

Stack multiple bindings — the most specific rule wins automatically.
channels:
  telegram:
    token: ${TELEGRAM_BOT_TOKEN}
    routes:
      default: general
    bindings:
      - { peer: "12345678", agent: vip }
      - { role: support, agent: support }
      - { channel_id: "-100999", agent: ops }
      - { chat_type: dm, agent: assistant }
A user with id 12345678 always gets vip. A support-role member who DMs the bot gets support (role beats chat type). The ops channel routes to the ops agent. All other DMs go to assistant. Everything else falls back to general.
4

Restrict tools by trust tier

Add trust: to any binding to scope the toolset the model sees — strangers get a safe subset, your operator keeps full power.
gateway:
  routes:
    - { chat_type: dm, agent: assistant, trust: untrusted }       # safe subset for strangers
    - { peer: "operator-123", agent: assistant, trust: trusted }  # full toolset for the operator
    - { channel_id: "ops", agent: assistant, deny_tools: [shell, delete_file] }
See Gateway Tool Policy for the full security reference.

How It Works

The gateway resolves the target agent in four deterministic steps:
StepWhat the gateway does
1Extracts peer, roles, channel_id, account, chat_type from the inbound message
2Tries every bindings: rule, keeps only the ones that match
3Sorts surviving matches: higher priority wins, then higher specificity, then declaration order
4If no binding matches, falls back to routes[chat_type], then routes["default"], then the literal agent id "default"

Routes vs Bindings


Configuration Options

Each entry in the bindings: list is a RouteBinding:
FieldTypeDefaultDescription
agentstrrequiredAgent id to route to when this binding matches
chat_typeOptional[str]None"dm" | "group" | "channel"
peerOptional[str]NoneSender/user id (most specific)
roleOptional[str]NoneRole / guild-role membership of the sender
channel_idOptional[str]NoneSpecific chat/channel id
accountOptional[str]NoneReceiving bot account (multi-account channels)
priorityint0Higher wins; ties broken by specificity then declaration order
trustOptional[str]NoneTrust tier — "untrusted" | "standard" | "trusted". untrusted applies a conservative deny-list (no shell, no file mutation, no delegation, no self-scheduling). Unknown values fail closed to untrusted.
allow_toolsOptional[List[str]]NoneOnly these tool names are exposed on this route. Accepts a YAML scalar or list.
deny_toolsOptional[List[str]]NoneTool names removed before the run on this route. Layers on top of the trust tier’s deny-list. Accepts a YAML scalar or list.
All non-None conditions in a binding must match the inbound message for that binding to apply. A binding with no conditions always matches. Specificity weights — when two bindings both match, the one with the higher total specificity wins:
FieldSpecificity weight
peer16 (most specific)
role8
channel_id8
account4
chat_type2
Ties on (priority, specificity) are broken by declaration order — the first matching binding in your list wins.

Common Patterns

from praisonaiagents import Agent
from praisonaiagents.gateway import RouteBinding, RouteFacts, resolve_route

vip = Agent(name="vip", instructions="You are the VIP concierge.")
general = Agent(name="general", instructions="You are the general assistant.")

bindings = [
    RouteBinding(agent="vip", peer="12345678"),
    RouteBinding(agent="general", chat_type="dm"),
]

facts = RouteFacts(chat_type="dm", peer="12345678")
match = resolve_route(bindings, facts, default_agent="general")

print(match.agent)   # "vip"
print(match.reason)  # "matched binding (priority=0, specificity=16)"
VIP customer gets a dedicated agent
channels:
  telegram:
    token: ${TELEGRAM_BOT_TOKEN}
    routes:
      default: general
    bindings:
      - { peer: "12345678", agent: vip }
Support-role members (Discord) get the support agent; everyone else gets general
channels:
  discord:
    token: ${DISCORD_BOT_TOKEN}
    routes:
      default: general
    bindings:
      - { role: support, agent: support }
Force an override regardless of specificity using priority
channels:
  telegram:
    token: ${TELEGRAM_BOT_TOKEN}
    routes:
      default: general
    bindings:
      - { peer: "12345678", agent: vip }
      - { chat_type: dm, agent: incident_responder, priority: 100 }
The incident_responder binding has priority: 100 so it wins over the peer match for user 12345678, even though peer has higher specificity. Use this for incident-mode overrides. Lock down stranger DMs while keeping operator at full power
channels:
  telegram:
    token: ${TELEGRAM_BOT_TOKEN}
    routes:
      default: assistant
    bindings:
      - { chat_type: dm, agent: assistant, trust: untrusted }
      - { peer: "operator-123", agent: assistant, trust: trusted }
Strangers who DM the bot never see shell, file-mutation, delegation, or scheduling tools. Your operator (peer: "operator-123") retains the full toolset. See Gateway Tool Policy for the complete security reference.

Best Practices

Bindings are evaluated first, but routes.default is the safety net when nothing matches. Without it, unmatched messages silently go to an agent named "default" — which may not exist.
routes:
  default: general   # always include this
bindings:
  - { peer: "12345678", agent: vip }
Let the resolver pick by specificity — peer beats role beats channel_id beats account beats chat_type. Reach for priority only when you genuinely need to override, such as an incident-mode binding that must win regardless of user identity.
Telegram numeric ids and Discord channel ids are stable. Display names and usernames change. Use the numeric id from the platform — never a username or handle.
If your list grows past ~10 entries, consider grouping users by role at the platform level and binding on role instead of individual peer ids. A long list of peer bindings is hard to audit and easy to break.

Bot Message Routing

The simpler chat-type routing surface — route by dm, group, or channel.

Multi-Channel Bots

Run one bot per role on the same platform using multiple channel entries.

Gateway Tool Policy

Full reference for trust-tiered toolset scoping — keep stranger DMs from running shell on your server.

Approval

Second line of defence — require human confirmation before risky tools run.