/routers);
each router opens at /routers/:agentId with the tabs Overview (overview), Routing
(routing), Access (access), Slack (slack) and Versions (versions). The same
routes are also visible from the target side on an agent’s or sandbox’s Routing tab
(/agents/:agentId/routing, /sandboxes/:agentId/routing).
What it does
A router is an agent.createRouterAgent creates an Agent with kind = router
(AgentKind: standard, router) in status draft, with a version 1 so the Versions tab has a
row. A router has no prompt, model, tools, knowledge, memory or sandbox: updateAgent on a
router accepts name, description, avatar and status only and rejects anything else with “Routers
have no prompt, model, tools or sandbox”. The router’s sidebar therefore has no Chat,
Instructions, Tools, MCP or Skills sections. Routers count against the company’s max_agents
limit like any agent, and the slug is derived from the name and must be unique in the company.
Agent lifecycle, versions and the Access table are described in Agents.
Routes. A route (RouterRoute) says: via router R, principal P lands on target agent A. The
principal is a RouterPrincipalKind: SLACK_USER (a Slack user id starting with U or W) or
OLYROS_USER (an active member of the company). One principal has at most one route per router
(adding a second fails with <person> is already routed to <agent> via this router). A target
can be a regular agent or a sandbox; it cannot be the router itself or another router (no chains).
Router and target must belong to the same company. An agent can sit behind several routers and
still keep its own Slack app.
Default target. setRouterDefaultTarget names the agent that people without a route land
on; passing no target clears it. With no default, an unrouted person is told “No agent is
configured for you via this assistant. Please contact a workspace admin.”
How a message to the router is dispatched. When someone DMs the router or mentions it in a
channel, the server runs these steps (resolve_slack_target):
- Router gate. The router’s own
agent.chatgrants (its Access table) are checked. A person the router denies gets the access-denied message. - Thread binding. If this Slack thread is already bound to a target, it goes there. A bound
target that is no longer
activefalls through to the next steps. - Route. A route for the Slack user wins; otherwise a route for the Olyros user that Slack user is linked to (the oldest such route).
- Default target, if set. Only
active, non-router agents of the router’s company qualify at steps 2 to 4; otherwise the person gets the “no agent is configured” message. - Target gate. The target’s own
agent.chatgrants are checked on every message, bound or not, so removing someone from a target’s Access table takes effect immediately. A denied person gets “You don’t have access to this agent. Please contact a workspace admin to be added.” - Bind. On first contact the thread is bound to the target, with reason
mapped(a route) ordefault(RouterBindingReason).
RouterBinding) pins one Slack thread to one target so a
conversation stays with its agent even if routes change later. One row is written per thread
(surface is slack, the only RouterSurface), keyed by workspace, channel and thread, recording
who started it (bound_by) and why (reason). Removing a route does not move threads already
bound to that target. In a DM, each top-level message starts its own thread and so its own
binding. routerBindings(routerAgentId, limit) lists a router’s bindings newest first, with the
target’s name.
How replies travel back. The target agent answers, but the reply leaves through the router’s
Slack bot, in the same thread, with the first chunk opened by the answering agent’s name in bold.
Rows the turn creates (questions to the user, approval cards, MCP connect cards, outbound
messages, sessions) record the router as the path the turn arrived on, so follow-up posts and
button clicks go through the same router. When a later post starts from an agent id alone, the
server picks the Slack connection in this order: the router the conversation came through; the
agent’s own app; the only connected router the agent is reachable through. If several connected
routers qualify and none is recorded, nothing is posted.
App Home of a router. A router’s Slack app always has an App Home tab, regardless of the
platform toggle for regular agents. It is rendered per viewer:
- The targets that viewer is routed to (their Slack-user route, their linked Olyros user’s route),
sorted by name, then the default target. Each is shown only if it is
activeand the viewer passes itsagent.chatcheck. A viewer the router itself denies sees no targets. - A sandbox target shows its status, any failure detail and the same buttons as the sandbox’s own App Home: Start or Stop by state, Rebuild, Open in Olyros, and Stop run while a turn is running. A regular agent shows its name, description, provider and model.
- At most 8 targets are shown. Beyond that, a line links to the router’s Routing page in Olyros. With no targets the tab says “No agents are available to you via this assistant. Ask a workspace admin to add a route.”
mentions_only; there is no “all messages” mode, so a
router acts on DMs and @mentions only. Installing the router’s Slack app uses a one-time, signed
install link: a link that is stale, reused or started by someone who is no longer a member is
refused, and the fix is to start the install again from the Slack tab. Details of the Slack
connection are in Slack.
Who can use it
Talking to a router in Slack needs the router’s
agent.chat grant and the target’s agent.chat
grant, decided per object by each agent’s Access table: an empty table is open to everyone, any
grant closes it. Roles do not bypass these checks. There is no per-feature plan gating; the only
plan limit that applies is the max_agents structural limit at creation. The roles are listed
on Company settings.
How to configure it
- Create the router. Agents -> Routers (
/routers) -> New router. Name is required (blank names are rejected), description is optional.createRouterAgent(name, description, avatarUrl). The app opens/routers/:agentId/overview. - Connect it to Slack. Slack tab (
/routers/:agentId/slack): create and install the Slack app as for any agent (Slack). The router’s app gets an App Home tab automatically. Optionally restrict the channel list here. - Set the default target (optional). Routing tab (
/routers/:agentId/routing) -> Default target select -> pick an agent or No default.setRouterDefaultTarget(routerAgentId, targetAgentId). - Add routes. Routing tab -> Add route: choose Olyros user (a member of this
company) or Slack users (picked from the Slack workspace), and a target agent.
addRouterRoute(routerAgentId, targetAgentId, principalKind, principalRef). Rejected when the target is the router itself or another router, the Slack id is not aU…/W…id, the Olyros user is not an active member, or the person already has a route on this router. The same form exists on the target’s own Routing tab, where it picks the router instead of the target. - Check the warnings. A route row shows a warning,
Not on <agent>'s allowlist; they will be denied, when the target’s Access table would refuse that person (onTargetAllowlistfalse). Fix it on the target’s Access tab. The warning is advisory: it does not evaluate Slack groups or linked accounts, and the dispatch-time check is authoritative. - Restrict who may use the router (optional). Access tab (
/routers/:agentId/access): add Olyros users, Slack users or Slack groups.setAgentAccess. Leaving it empty keeps the router open; the targets still gate. - Publish. Header Publish (
publishAgentVersion(targetEnv: production)) makes the routeractive; Move to draft returns it todraft. Bothactiveanddraftrouters dispatch Slack messages; apausedorarchivedrouter ignores them. - Remove a route. Routing tab -> remove on the row.
removeRouterRoute(routeId). Threads already bound to that target stay bound.
Limits and defaults
What it emits
- Route rows with creator and creation time, listed per router and per target by
routerRoutes. - Thread binding rows, one per routed Slack thread, with target, starter and reason
(
mappedordefault), listed byrouterBindings. - The path a turn took. Interactions, sessions, approval requests, outbound Slack messages,
pending messages and App Home lifecycle actions carry the router as
via_agent_idwhen the turn or click came through a router. The rows themselves belong to the target agent, so its executions, costs and audit history show routed traffic as the target’s. - Server log lines for each routed message with its outcome (
routed,router_denied,no_route,target_denied); there is no routing log in the product UI.
Known gaps
- No page shows thread bindings:
routerBindingsexists in the API but no client-app screen calls it. - No per-route or per-target routing log or counters in the UI; outcomes go to server logs only.
- Routing is by person only. Routing by channel, user group, keyword or message content is not built.
- Replies post under the router’s name with a text prefix; posting under the target’s own name and avatar is planned. The app requests the scope for it already.
- A person cannot pick or switch the agent inside a thread; the binding is fixed at first contact.
- Bindings never expire, and every top-level DM message adds one; retention is undecided.
- Slack is the only surface. Microsoft Teams and web chat through a router are planned.
- The workflow Slack message node’s agent picker (
slackCapableAgents) still lists routers that have a Slack connection. - Other agent mutations (tools, knowledge, MCP, skills, duplicate, conversion to sandbox) do not reject a router id at the API; only the pickers hide routers.
- Background start, stop and rebuild outcome rows do not record the router; click-time audit rows do.
- Edit, publish and archive of a router have no role gate beyond company membership, unlike routes and access, which need editor or admin.
- When an agent is reachable through several connected routers and a post has no recorded router, nothing is posted (logged as ambiguous).