# Slack agents

A Slack agent connects your own Slack app to RouterPlus. People mention the app in a channel or
send it a direct message. RouterPlus answers in the same thread, with the model you select.

Each agent has its own RouterPlus inference key. Every answer is an ordinary request on that
key. Your guardrails, budgets, routing and billing apply.

## Set up an agent

1. Create a Slack app at api.slack.com/apps:
   - Select "From scratch" and install the app in your workspace.
   - Copy the bot token (`xoxb-...`) from "OAuth & Permissions".
   - Copy the signing secret from "Basic Information".
2. Create the agent with a management key (`Authorization: Bearer tm_mk_...`):

```
POST /v1/slack/agents
```

```json
{
  "slug": "helpdesk",
  "bot_token": "xoxb-...",
  "signing_secret": "8f74...",
  "model": "gpt-6-luna",
  "system_prompt": "You answer questions about our product.",
  "allowed_channel_ids": ["C0123ABCD"],
  "allowed_user_ids": [],
  "tools": { "server": [{ "type": "web_fetch" }], "mcp_servers": ["docs"] },
  "max_cost_usd_per_day": 10
}
```

3. Get the app manifest:

```
GET /v1/slack/agents/{id}/manifest
```

4. In your Slack app, open "App Manifest", paste the manifest and save. Slack checks the events
   URL. When Slack asks, install the app again to give it the new scopes.
5. Invite the app to a channel (`/invite @helpdesk`). Mention it, or send it a direct message.

`POST` returns `201` with the agent. Slack sends events to the agent's `events_url`:
`https://<gateway>/v1/slack/events/{id}`.

## Fields

| Field | Rule |
|---|---|
| `slug` | Required. 1 to 40 of `a-z`, `0-9` and `-`, starting with a letter or digit. Unique in your org. |
| `bot_token` | Required. The bot token, `xoxb-...`. Stored encrypted. Never returned. |
| `signing_secret` | Required. The app's signing secret. Stored encrypted. Never returned. |
| `model` | Required. A model id, a preset (`@preset/<slug>`) or `routerplus/auto`. |
| `system_prompt` | Optional. Up to 8,000 characters. |
| `allowed_channel_ids` | Optional. Channel ids where the agent answers. Empty (the default) means every channel that the app is in. This list does not limit direct messages. |
| `allowed_user_ids` | Optional. User ids that the agent answers, in channels and in direct messages. Empty (the default) means anyone in the workspace. |
| `tools.server` | Optional. Server tools: `{ "type": "web_fetch" }` with optional `allowed_domains`, `max_bytes` and `max_calls`, and `{ "type": "advisor", "model": "..." }`. Not with `routerplus/auto`. See [Server tools](server-tools.md). |
| `tools.mcp_servers` | Optional. Slugs of your MCP servers. The agent calls them through the [MCP gateway](mcp-gateway.md) with its own key, so their tool policies apply. |
| `max_cost_usd_per_day` | Optional. The most that the agent's key spends in one UTC day. `null` (the default) means no daily limit. |
| `workspace_id`, `principal_id` | Optional, on create only. The workspace and principal of the agent's key. Default: your default workspace. |
| `slack_team_id`, `slack_app_id` | Optional. When set, the agent ignores events from another Slack workspace or app. |
| `enabled` | Optional. Default `true`. A disabled agent acknowledges events but does not answer. |

## Manage agents

| Route | What it does |
|---|---|
| `GET /v1/slack/agents` | Lists your agents. |
| `POST /v1/slack/agents` | Creates an agent and its key. |
| `GET /v1/slack/agents/{id}` | Reads one agent. |
| `PATCH /v1/slack/agents/{id}` | Changes the fields you send. Send `bot_token` or `signing_secret` to rotate it. |
| `DELETE /v1/slack/agents/{id}` | Deletes the agent and its thread memory, and disables its key. |
| `GET /v1/slack/agents/{id}/manifest` | The Slack app manifest for the agent. |

- Every route also works under `/api/v1`.
- Each change writes a row to the audit log (`GET /v1/audit`).
- Responses show `has_bot_token` and `has_signing_secret`, never the values.
- The agent's key is a normal inference key with the name `slack:<slug>`. It shows in
  `GET /v1/keys` and in activity. To stop all spend immediately, disable the key or the agent.

## Scopes and events

The manifest asks for these bot scopes:

| Scope | Use |
|---|---|
| `app_mentions:read` | Receive mentions of the app. |
| `chat:write` | Post answers. |
| `im:history`, `im:read`, `im:write` | Receive and answer direct messages. |
| `channels:history` | Read messages in threads of public channels. |

It subscribes to two bot events: `app_mention` and `message.im`.

## How the agent answers

- A mention in a channel gets an answer in the thread of that message.
- A direct message gets an answer in the direct message. Direct messages outside a thread are
  one conversation.
- The agent does not answer bots (itself included), edits, deletions, or people who are not on
  the allow lists.
- RouterPlus removes the app mention from the text before the model sees it.
- The model sees the system prompt, the thread's earlier messages (up to 30), and the new
  message.
- When the agent's key has reached `max_cost_usd_per_day` today, the agent posts a short
  notice and does not call the model.
- When the model call fails, the agent posts a short message, for example "Sorry, I could not
  answer that. Try again later."

## Security

- RouterPlus checks Slack's signature on every request. The signature is an HMAC-SHA256 of
  the timestamp and the body, with your signing secret. A request without a valid signature gets
  `401`.
- A request with a timestamp more than 5 minutes from now gets `401`. Thus nobody can send a
  captured request again.
- Slack sends an event again when it gets no acknowledgement in time. RouterPlus answers each
  event id one time.
- RouterPlus acknowledges each event immediately and answers in the background.

## What is stored

| Data | How long |
|---|---|
| Bot token, signing secret, agent key | Encrypted, until you delete the agent. |
| Thread memory: the last 30 messages of each thread, from people and from the agent | 7 days after the thread's last message, then deleted. |
| Event ids, to answer each event once | 1 day. |

RouterPlus keeps the model requests like any other request on your key. See
[Data policy](data-policy.md).
