> ## Documentation Index
> Fetch the complete documentation index at: https://user-docs.olyrosstudios.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Routers

> A router lets your company install one Slack app and put many agents and personal sandboxes behind it, so adding an agent no longer needs a new Slack app.

A router is one Slack app that sits in front of many agents and sandboxes. Instead of installing
a new Slack app for every agent (often an IT approval each time), a company installs the router
once and adds routes: "via this router, this person lands on that agent". A person who DMs or
mentions the router is handed to their routed agent, the target's own access rules still decide
whether they may talk to it, and the router's App Home in Slack shows and controls the sandboxes
that person can reach. Routers live in the client app under **Agents -> Routers** (`/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](https://user-docs.olyrosstudios.com/agents.md).

**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`):

1. **Router gate.** The router's own `agent.chat` grants (its Access table) are checked. A person
   the router denies gets the access-denied message.
2. **Thread binding.** If this Slack thread is already bound to a target, it goes there. A bound
   target that is no longer `active` falls through to the next steps.
3. **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).
4. **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.
5. **Target gate.** The target's own `agent.chat` grants 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."
6. **Bind.** On first contact the thread is bound to the target, with reason `mapped` (a route)
   or `default` (`RouterBindingReason`).

Both refusal messages are posted ephemerally, visible only to the sender. A route routes but never
admits: the access check on the target is the authority. How grants are matched, including
Slack groups and linked Olyros users, is described on [Agents](https://user-docs.olyrosstudios.com/agents.md).

**Thread bindings.** A binding (`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 `active` and the viewer
  passes its `agent.chat` check. 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."

Button clicks on a router's home are checked twice: the clicker must pass the router's own
Access table, and the target must be reachable through this router (a route, the default
target or a thread binding) in the same company. The target's own access check then runs as for
a click on its own App Home. When a sandbox changes state, the App Home of every router that
reaches it is refreshed for viewers seen in the last 5 minutes. Sandbox lifecycle is described in
[Sandboxes](https://user-docs.olyrosstudios.com/sandboxes.md).

**Channel list and installation.** A router's channel list is enforced
on the router's own Slack connection: when the list is non-empty, a mention in a channel not on
it gets an ephemeral "I'm not enabled in this channel." and is not routed. DMs are never limited
by the list. Trigger mode accepts only `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](https://user-docs.olyrosstudios.com/slack.md).

## Who can use it

| Action | Operation | Who |
| - | - | - |
| See the Routers list and a router's pages | `agents(kind: ROUTER_ONLY)`, `agent(agentId)` | any company member |
| See a router's routes, or the routes that land on an agent | `routerRoutes(routerAgentId, targetAgentId)` | any company member |
| See a router's thread bindings | `routerBindings(routerAgentId, limit)` | any company member |
| Create a router | `createRouterAgent` | editor or admin (`agent.manage`) |
| Set or clear the default target | `setRouterDefaultTarget` | editor or admin (`agent.manage`) |
| Add or remove a route | `addRouterRoute`, `removeRouterRoute` | editor or admin (`agent.manage`) |
| Edit who may use the router (Access tab) | `setAgentAccess` | editor or admin (`agent.manage`) |
| Edit name and description, publish, move to draft, archive, restore | `updateAgent`, `publishAgentVersion`, `archiveAgent`, `restoreAgent` | any company member (no explicit role gate) |

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](https://user-docs.olyrosstudios.com/company-settings.md).

## How to configure it

1. **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`.
2. **Connect it to Slack.** **Slack** tab (`/routers/:agentId/slack`): create and install the
   Slack app as for any agent ([Slack](https://user-docs.olyrosstudios.com/slack.md)). The router's app gets an App Home tab
   automatically. Optionally restrict the channel list here.
3. **Set the default target (optional).** **Routing** tab (`/routers/:agentId/routing`) ->
   **Default target** select -> pick an agent or **No default**. `setRouterDefaultTarget(routerAgentId,
   targetAgentId)`.
4. **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 a `U…`/`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.
5. **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 (`onTargetAllowlist` false).
   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.
6. **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.
7. **Publish.** Header **Publish** (`publishAgentVersion(targetEnv: production)`) makes the router
   `active`; **Move to draft** returns it to `draft`. Both `active` and `draft` routers dispatch
   Slack messages; a `paused` or `archived` router ignores them.
8. **Remove a route.** **Routing** tab -> remove on the row. `removeRouterRoute(routeId)`.
   Threads already bound to that target stay bound.

## Limits and defaults

| Setting | Default | Where enforced |
| - | - | - |
| Status of a new router | `draft`, version 1 | `createRouterAgent` |
| Routers per company | counted in `max_agents` with all non-archived agents | structural billing gate in `createRouterAgent` |
| Routes per principal per router | 1 | unique constraint `uq_router_routes_principal` |
| Slack principal format | `U` or `W` followed by at least two letters or digits | route validation |
| Default target | none | `agents.default_target_agent_id`, set by `setRouterDefaultTarget` |
| Bindings per thread | 1 | unique constraint `uq_router_bindings_thread` |
| `routerBindings` page size | 50, clamped to 1 to 200 | `routerBindings` query |
| `routerRoutes` filter | router or target required | `list_routes` |
| Targets on the App Home | 8 per viewer | App Home builder |
| App Home refresh after a sandbox state change | viewers seen in the last 5 minutes | App Home publisher |
| Trigger mode | `mentions_only` only | Slack app settings |
| Router target status | `active` only | routing resolver |
| Router status that dispatches | `active` or `draft` | Slack event handling |

## 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
  (`mapped` or `default`), listed by `routerBindings`.
* **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_id` when 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: `routerBindings` exists 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).

## Customer-safe summary

A router lets your company install one Slack app and put many agents and personal sandboxes
behind it, so adding an agent no longer needs a new Slack app. An editor or admin adds routes
that send each person, by Slack account or Olyros account, to their agent, and can set a default
agent for everyone else. Each agent's own access rules still decide who may talk to it, and the
router can have its own access list as an extra gate. Replies come back in the same thread under
the router, labelled with the name of the agent that answered, and a conversation stays with its
agent once it starts. The router's App Home in Slack shows each person the agents and sandboxes
they can reach, with start, stop and rebuild controls for sandboxes.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.