arizuko

arizuko › concepts › routing

routing

Routing decides which folder runs an inbound message. When a message arrives, routd reads a table of rules. Each rule says “if this message looks like X, run it in folder Y”, and the first match wins.

One table maps messages to folders

Every message from Telegram, Discord, Slack, email or the web widget goes into the same messages table of routd. routd then asks one question: which folder runs this message? The answer comes from the routes table, with one row per rule, in order:

seq    match                                  target
---    -----                                  ------
-10    chat_jid=telegram:111/user/12345       atlas/legal
0      platform=telegram                      atlas/content
0      platform=discord room=dm/*             atlas/dm
0      platform=slack verb=mention            atlas/mentions
0      chat_jid=slack:T1234/channel/C5678     solo/chat
0      chat_jid=hook:acme/eng/github          acme/eng#observe
0      platform=discord                       atlas/{sender}
9999   (empty)                                atlas              # catchall

A lower seq goes first. The empty match at seq=9999 matches every message, so it is the fallback.

Every test in a rule must pass

The match column is a list of key=value tests, separated by spaces, and all of them must pass. For example, platform=discord room=dm/* matches a Discord message in a DM and nothing else. The value is a glob that stops at /. For this reason, discord:dm/* covers every DM but never matches a guild channel such as discord:123/456. reference / jid has the full glob grammar.

A rule can use five keys:

platform
The part before the colon, for example telegram.
room
The part after platform:.
chat_jid
The whole chat address.
sender
The address of the sender.
verb
The action, for example post or like.

If a rule leaves a key out, that field can have any value.

The verb key has no fixed list of values. Each adapter sends its own words, and routd adds a few. Telegram, Discord and Slack send mention when someone writes @bot. A reaction arrives as like or dislike. A rule for a verb that never arrives matches nothing and reports no error. ROUTING.md has the table of verbs for each adapter.

One rule gives each user a folder

If the target contains {sender}, routd replaces it with a sanitized sender ID. The rule platform=discord with the target atlas/{sender} gives every Discord user a child folder, such as atlas/dc-alice and atlas/dc-bob, with no route per person.

A reply to the bot goes back to the folder that sent it

When the bot replies in a chat, routd records the folder that produced the reply in the routed_to field of the message row. If a user then replies to that bot message, routd follows the chain back to that folder. The folder must be a child that the current folder can route to. So a thread stays in the folder that started it.

Users can pin a chat to a folder or a topic

A user can pin a chat to a folder with a message that contains only @atlas/social. routd stores the pin on the chats row, and every later message in that chat goes to atlas/social. A message that contains only @ clears the pin. #support pins the topic in the same way, and a bare # clears it.

If the folder after @ does not exist, routd gives the message to the agent unchanged, with no confirmation and no error. A message that starts with @ has many other uses, so routd does not consume it on a miss. Examples are a mention like @everyone, a pointer to another instance like @sloth, or a word in a language that uses @. The agent decides if the text is a typo for a child folder, a real mention or plain text. For a typo, it calls delegate_group.

Agents pass work along the folder tree. delegate_group sends a prompt to a folder that the grant of the caller covers, usually a child, and escalate_group sends one up to the parent. The child turn runs in a guest of its own, as a turn, end to end shows. Both tools refuse a call with a depth argument of 1 or more, and no code passes a depth on, so grants and not depth bound a chain of hand-offs. observe_group only reads.

At the start of a message, @name something delegates that one message to the child folder <current>/name. #topic something runs that one message under the topic. Neither prefix changes the pinned state.

routd resolves the folder in three checks

routd picks the folder with three checks, in this order:

  1. Direct address. A web chat or a hook token names its folder, and a JID with no : is a folder name. The route table never sees these messages.
  2. The route table. The first matching row by seq wins. If the chat and topic pair is engaged, the engaged folder wins over the folder of the rule for a message or mention from a person. An #observe rule then starts a turn anyway. Every other verb follows its rule.
  3. Engagement. Engagement can catch a message or mention that no rule matched. A thread without its own engagement window uses the window of its root message, unless someone disengaged that thread.

One chat can hold several unread rows at once. routd reads the route table once for the batch and picks the newest row that starts a turn on its own. A mention therefore starts a turn even when a newer row of the batch matches only #observe. The turn sees the other rows as observed context.

After routd has a folder, a second pass runs inside it. The pass checks the pinned @ and #, the slash commands, an inline @child or #topic prefix, and then a delegation target. The delegation target is the folder of a replied-to bot message, the pinned group or the route table, in that order. A delegation goes only to a child that the current folder can route to. Any step in the pass can consume the message before a turn starts. A turn, end to end shows the whole path from the adapter to the reply.

A route change applies to the next message

The agent edits the table with the MCP tools add_route, delete_route, list_routes and set_routes. The operator dashboard dashd shows the same table in a browser. A caller with an operator REST token can call /v1/routes directly. One handler serves all of these, so the agent and a REST caller see the same rows. A change applies to the next inbound message, with no restart.

A target tail sets the mode or the topic

A target can end with #.... That tail tells routd what to do after the message arrives in the folder. Three tails are reserved: #observe, #engaged and #announce. Every other tail is a topic name for the routed message.

targeteffect
folderStores the message under folder and starts a turn on the main topic. This is the default.
folder#observeStores the message under folder and starts no turn. A later turn sees the message in the history, with the full ACL of the folder. If the chat and topic pair is engaged, routd ignores this mode for a message or mention from a person and starts a turn. Other verbs stay observed. See engagement.
folder#engagedStarts a turn while the chat and topic pair is engaged, for any verb, and stores the message under folder as observed otherwise. It never addresses the agent. Use it to let CI results continue an open conversation.
folder#announceStarts a turn as usual and marks a concrete channel as a release-note destination. On startup, routd queues the release notice for each configured destination. With no #announce route, it queues no notice. Remove the tail to stop the posts.

Combine a tail with verb=mention and seq to answer only mentions. One row starts a turn on mentions, and a catch-all row observes everything else. An example for Discord, where 1100223344 is the ID of one guild:

seq  match                                       target
10   platform=discord room=1100223344/*          main
20   platform=discord verb=mention               main
30   platform=discord                            main#observe

Row 10 starts a turn on every message in guild 1100223344. Row 20 starts a turn on a mention anywhere else on Discord. Row 30 stores the rest in main as context and does not start the agent.

ROUTING.md has the full rules

[ROUTING.md] has the full rules: the reply chain, authorization across folders, and complete traces. jid shows the address strings in the match column. topics shows how #topic isolates a thread inside one folder.