how a message lands in a folder · ← concepts
When a message arrives, the gateway walks a small table of rules. Each rule says “if this message looks like X, run it in folder Y.” First match wins.
Every message arriving from Telegram, Discord, Mastodon, email, or the web widget hits the same store. The gateway then asks one question: which folder runs this? The answer comes from the routes table — one row per rule, evaluated in order:
seq match target
--- ----- ------
-10 chat_jid=telegram:user/12345 atlas/legal
0 platform=telegram atlas/content
0 platform=discord room=dm/* atlas/dm
0 platform=reddit verb=post atlas/posts
0 chat_jid=web:acme solo/chat
0 chat_jid=hook:acme/eng/github acme/eng#observe
0 platform=discord atlas/{sender}
9999 (empty) atlas # catchall
Lower seq goes first. The empty match at seq=9999 matches every message, so it acts as the fallback.
The match column is a space-separated list of key=value tests, all of which must pass. So platform=discord room=dm/* matches a Discord message whose chat is a DM, and nothing else.
Values use path.Match globs: * matches any run of characters except /, ? matches one character, [abc] matches a class. That’s why discord:dm/* covers every DM but doesn’t cross into discord:guild/123/channel/456.
Five keys are available: platform (e.g. telegram), room (the part after platform:), chat_jid (the whole address), sender (who sent it), and verb (the action, e.g. post, like). Omit a key and that field is unconstrained.
Write {sender} in the target and the gateway expands it to a sanitized sender ID. One rule like platform=discord → atlas/{sender} gives every Discord user their own child folder — atlas/dc-alice, atlas/dc-bob — without one route per person.
When the bot replies in a chat, the gateway records which folder produced the reply on the message row (routed_to). If the user then replies to that bot message, the gateway follows the chain back to the same folder, even if the route table says otherwise. So a thread stays in the folder that started it.
A user can pin a chat to a folder by sending @atlas/social as the entire message — the gateway stores it on the chats row and every later message in that chat goes to atlas/social until someone sends bare @ to clear it. #support does the same for the topic; bare # clears.
If the folder after @ doesn’t exist, the gateway leaves the message alone and hands it to the agent unchanged — no confirmation, no error. Messages starting with @ have too many other uses (a mention like @everyone, a cross-instance pointer like @sloth, a sentence in a language where @ is just a word) for the gateway to swallow them on a miss. The agent decides whether it’s a typo for a child folder (and calls delegate_group), a real mention, or plain text.
Inline at the start of a message, @name something delegates that one message to the child folder <current>/name, and #topic something runs it under that topic — for one message only, sticky state untouched.
Put together, every inbound message goes through these layers in order:
(chat, topic) is engaged, route to the engaged folder and fire a turn. All layers below are skipped.routed_to.The inline @name/#topic prefix is handled separately before the route table, so it can override a default route without touching sticky state.
The agent edits the table through the MCP tools add_route, delete_route, get_routes, set_routes. The operator dashboard (dashd) shows the same table in a browser. Changes take effect on the next inbound message — no restart.
The target column carries an optional # fragment that decides what the gateway does after the message lands in the folder. #observe is the only reserved fragment — it sets a mode. Every other fragment is a topic name pinned for the routed message.
| target | effect |
|---|---|
folder | store under folder and fire a turn on main (default) |
folder#observe | store under folder, do not fire a turn. The agent still sees the message in history when a later turn runs and has full folder ACL. Exception: engagement overrides all route targets — if the (chat, topic) is engaged, the gateway ignores this mode and fires a turn anyway. See engagement. |
folder#deploy | store under folder with topic #deploy and fire a turn there. Useful for piping a webhook source into a dedicated topic without operator @-prefixes. |
Combine with verb=mention and seq to build a two-row mention-only pattern: a tight row triggers on mentions, a catch-all row observes everything else. Sloth Discord guild:
seq match target
10 platform=discord room=guild/sloth main
20 platform=discord room=guild/* verb=mention main
30 platform=discord room=guild/* main#observe
Row 10 fires on every message in the named guild. Row 20 fires on @mentions in any other guild. Row 30 silently archives the rest into main for context without invoking the agent.
Future modes (#drop, #digest, …) extend the fragment vocabulary without schema changes.
Full rules — reply-chain mechanics, cross-folder authorization, worked end-to-end traces — live in ROUTING.md. For what the address strings on the left side actually look like, see jid. For how #topic isolates a thread inside one folder, see topics.