Slack Bot Integration
Run a full two-way Slack bot from your VM, with the tokens held off-VM
The Slack Bot integration lets a VM drive a real Slack bot: post as the bot,
read history, react, and respond live to mentions and DMs — while the bot's
tokens stay off the VM entirely. You bring your own Slack app; exe.dev holds
its tokens server-side and injects them on the way to Slack.
This is the two-way sibling of the send-only
[Slack Integration](integrations-slack). Use that one if all you need is
"post a message to a channel."
## How it works
Your VM talks to the integration hostname exactly as it would talk to
`slack.com`:
```
curl -X POST https://mybot.int.exe.xyz/api/chat.postMessage --json '{"channel":"C0123","text":"hi"}'
```
exe.dev forwards `/api/<method>` calls to the Slack Web API with your bot
token attached. Any Web API method works; what the bot may actually do is
bounded by the scopes you granted your own app.
For *receiving* events (mentions, DMs), the integration supports Slack's
[Socket Mode](https://api.slack.com/apis/socket-mode): the VM calls one
special method, `apps.connections.open`, and gets back a single-use ticketed
`wss://` URL. The VM connects to that URL directly — an outbound WebSocket,
no inbound ports, no tokens — and events stream in over it.
## Step 1: create your Slack app
Go to [api.slack.com/apps](https://api.slack.com/apps) → **Create New App** →
**From an app manifest**, pick your workspace, and paste:
```json
{
"display_information": { "name": "my-exe-bot" },
"features": {
"bot_user": { "display_name": "my-exe-bot", "always_online": true }
},
"oauth_config": {
"scopes": {
"bot": ["chat:write", "app_mentions:read", "im:history"]
}
},
"settings": {
"event_subscriptions": {
"bot_events": ["app_mention", "message.im"]
},
"socket_mode_enabled": true
}
}
```
This is the minimal talk-to-able bot: it can post, and it hears mentions and
DMs. Add scopes as your bot needs them (`channels:history` to read channels,
`reactions:write` to react, `files:write` to upload, ...). Each event
subscription needs its matching read scope or Slack won't deliver it.
Then collect two tokens:
1. **Bot token** (`xoxb-…`): **Install App** to your workspace, then copy the
Bot User OAuth Token from **OAuth & Permissions**.
2. **App-level token** (`xapp-…`), only needed for receiving events:
**Basic Information → App-Level Tokens → Generate**, with the
`connections:write` scope.
Finally, invite the bot to a channel: `/invite @my-exe-bot`.
## Step 2: create the integration
On the [Integrations page](/integrations), click the **Slack Bot** tile and
paste the tokens — the Test button verifies them against Slack and fills in
the bot's name. Or via SSH, passing `-` so the tokens are
[never on the command line](integrations#providing-secrets-to-integrations-commands).
At the interactive prompt, each `-` asks for its token with hidden input:
```
exe.dev ▶ integrations add slack --name mybot --bot-token=- --app-token=-
Bot token (xoxb-...):
App-level token (xapp-...):
exe.dev ▶ integrations attach mybot vm:my-vm
```
Scripted (`ssh exe.dev integrations add ...`), stdin carries the tokens, one
per line — bot token first:
```
$ printf '%s\n%s\n' "$SLACK_BOT_TOKEN" "$SLACK_APP_TOKEN" | \
ssh exe.dev integrations add slack --name mybot --bot-token=- --app-token=-
```
The `--app-token` is optional; without it the bot can call the Web API but
not receive events.
## Step 3: use it from the VM
The examples below use the integration hostname
`mybot.int.exe.xyz` — substitute the exact URL printed when you created the
integration (it varies by integration name and environment).
Post as the bot:
```
curl -X POST https://mybot.int.exe.xyz/api/chat.postMessage \
--json '{"channel":"C0123456789","text":"hello from my VM"}'
```
Sanity-check the token routing:
```
curl -X POST https://mybot.int.exe.xyz/api/auth.test
```
Receive events over Socket Mode. Standard Slack SDKs work once pointed at the
integration host; here is the bare-bones version with just `websockets`:
```python
import asyncio, json, urllib.request, websockets
GW = "https://mybot.int.exe.xyz/api/" # your integration URL (as printed on creation) + /api/
def gw_post(method, payload=None):
data = json.dumps(payload).encode() if payload is not None else b""
req = urllib.request.Request(GW + method, data=data,
headers={"Content-Type": "application/json"}, method="POST")
with urllib.request.urlopen(req) as resp:
return json.load(resp)
async def main():
url = gw_post("apps.connections.open")["url"] # ticketed wss URL
async with websockets.connect(url) as ws:
async for raw in ws:
msg = json.loads(raw)
if "envelope_id" in msg: # ack everything
await ws.send(json.dumps({"envelope_id": msg["envelope_id"]}))
ev = msg.get("payload", {}).get("event", {})
if ev.get("type") == "app_mention":
gw_post("chat.postMessage",
{"channel": ev["channel"], "text": "you rang?"})
asyncio.run(main())
```
Mention the bot in Slack and it answers — and at no point did an `xoxb-` or
`xapp-` token exist on the VM. The only credential the VM ever holds is the
single-use WebSocket ticket.
## Notes
- Slack rate-limits the Web API per method. When Slack says slow down,
exe.dev honors the `Retry-After` and rejects further calls to that method
locally until the window passes; other methods are unaffected.
- `apps.connections.open` without a stored app token returns 409 with the fix
(`integrations edit mybot --app-token=-`).
- Slack asks Socket Mode clients to reconnect periodically; SDKs handle this
automatically. If you hand-roll, just re-open a fresh ticket when the
socket closes.
- Slack allows at most 10 concurrent Socket Mode connections per app.
- Rotating tokens (e.g. after adding scopes, which requires reinstalling the
app): `integrations edit mybot --bot-token=- --app-token=-`.