Slack channel
# Slack channel
Slack uses the Events API, not Socket Mode. BONNIE receives signed events at `POST /slack/events` and replies through `chat.postMessage` with the bot token.
## Agent setup
```go
package main
import (
"github.com/mark3labs/bonnie"
"github.com/mark3labs/bonnie/channel/slack"
)
func main() {
bonnie.New(
bonnie.WithSlack(slack.Config{Activity: slack.ActivityMessage}),
).Serve()
}
```
```sh
export SLACK_BOT_TOKEN='xoxb-REPLACE_ME'
export SLACK_SIGNING_SECRET='REPLACE_ME'
# Also set the model provider key.
bonnie dev --addr 127.0.0.1:8081 --tui=false
```
Empty config credentials come from these variables. Nonempty config values take precedence. The root option requires both. Low-level `slack.New` requires a signing secret but permits no bot token; that mode cannot deliver replies.
`Path` overrides `/slack/events`. `CancelPath` defaults to `Path + "/cancel"`. `APIURL`, or the root option's `SLACK_API_URL` fallback, overrides `https://slack.com/api`; normally leave it unset.
## Slack app and permissions
1. Create a Slack app for the workspace.
2. Add bot token scopes below.
3. Install the app and obtain its bot token and signing secret.
4. Start BONNIE behind public HTTPS or a tunnel.
5. Enable Event Subscriptions and set Request URL to `https://HOST/slack/events`.
6. Subscribe to the needed bot events and reinstall when scopes change.
7. Invite the bot into each channel it must use.
| Bot scope | Use |
| --- | --- |
| `app_mentions:read` | Invocation mentions |
| `chat:write` | Replies and default activity placeholder |
| `im:history` | Direct messages |
| `channels:history` | Public-channel thread replies |
| `groups:history` | Private-channel thread replies |
| `assistant:write` | Only for `ActivityStatus` on Slack's assistant surface |
Subscribe to `app_mention`, `message.im`, `message.channels`, and `message.groups` for those surfaces. Mention-only setup cannot receive mention-free follow-ups in channel threads. The server must be running when Slack sends URL verification.
For native cancellation, register a Slack slash command with Request URL `https://HOST/slack/events/cancel` and the app's command scope, `commands`. Its visible command name is your Slack registration; the handler uses its form fields, not its name.
## Dispatch and addresses
| Event | Admission | Channel-local address |
| --- | --- | --- |
| `app_mention` | Always, unless bot/subtype filtering rejects it | `/`; root message `ts` starts a thread |
| `message`, `channel_type: "im"` | Every normal text DM | `/dm` |
| Threaded `message` | Only if the thread is already bound | `/` |
| Unbound thread without mention | Ignored | None |
The durable form adds `slack/`. A DM is `slack/D123/dm`, not just the channel ID. Replies to DMs are not threaded. Any event with `bot_id` or nonempty `subtype` is ignored, including edits. Slack mention tokens `<@USER_ID>` are removed from mention input.
A follow-up uses the same saved binding after a restart. Run IDs are separate; use `bonnie runs list --journal .bonnie` to find them. A waiting run posts its prompt; the next admitted text in the same conversation answers it. Slack approval buttons are not implemented: answers are text.
Use exact text controls such as `/new`, `/clear`, `/compact`, and `/help` after the message passes admission. In the Slack UI, a leading slash can invoke a platform command instead of sending text. A mention such as `@your-bot /new` produces the shared control after mention removal. Native cancellation avoids this ambiguity.
## Native cancel
In a public or private channel, supply the exact thread timestamp:
```text
/your-cancel-command 1700000000.000900
```
The signed form must carry `channel_id`, `user_id`, `text`, and `trigger_id`. An empty `text` is allowed only for a DM channel whose ID starts with `D`. A nonempty target must be a numeric `seconds.fraction` timestamp. Duplicate trigger IDs are suppressed in the process so a retry cannot cancel later work. The response reports cancellation requested or no active turn; it is not completion confirmation.
## Wire and verification
The adapter reads this event shape; Slack supplies and signs it:
```json
{
"type": "event_callback",
"event_id": "Ev123",
"event": {
"type": "app_mention",
"text": "<@U_BOT> Review the deployment plan.",
"ts": "1700000000.000900",
"channel": "C123",
"user": "U123"
}
}
```
Other read fields are top-level `challenge` and event `thread_ts`, `channel_type`, `bot_id`, and `subtype`. `url_verification` returns `{"challenge":"..."}` after verification. Event callbacks are acknowledged with 200 before agent execution.
Required headers are `X-Slack-Signature` and `X-Slack-Request-Timestamp`. The signature is `v0=` plus HMAC-SHA256 over `v0::`. Modified bodies fail verification with 401. The implementation rejects timestamps older than five minutes; it does not separately reject future timestamps. Keep clocks correct and do not alter signed bodies in a proxy. Bodies are capped at 1 MiB.
`event_id` deduplication is bounded and process-local. A restart or cache reset forgets IDs. It is not durable exactly-once delivery. The principal records authenticator `slack`, kind `user`, the event's user ID, and channel attribute. The signature proves Slack sent it, not that the user may run a sensitive tool or answer an approval. Add host authorization rules.
## Activity and delivery limits
- `ActivityMessage` is the default: one placeholder in the thread, edited and deleted before the final reply.
- `ActivityStatus` uses Slack's assistant status API; it needs that surface and `assistant:write`.
- `ActivityOff` disables the indicator.
Activity is best-effort. It can include reasoning and tool argument labels; decide whether that information is suitable for the conversation. Replies are plain text with markdown parsing disabled, not Block Kit. Attachments and files are ignored.
The current splitter uses a 39,000-byte part budget and at most five parts. Do not rely on a character-count guarantee or a truncation marker; excess text can be cut. Ordinary post failures are logged, not handled by a durable retry queue. The saved run remains available for inspection.
## Hand-offs and schedules
`Receive(ctx, "C123", text, opts)` opens a new thread by posting the instruction, binds its address before execution, then runs the agent there. It is not a plain notification. A refused root post is returned as an error and must not start a turn.
Slack also implements tracked schedule delivery. Use a string channel ID as the schedule target. Scheduled threads use the public root `Scheduled task`, not the private prompt. Saved results are retried on delivery failure; posts can repeat after a crash. See [schedules](/channels/overview#scheduled-channel-work).
## Source and tests
[Slack godoc](https://pkg.go.dev/github.com/mark3labs/bonnie/channel/slack). Setup permissions are also shown in `examples/slack-bot/README.md`. Source: `slack.go`, `cancel.go`, `activity.go`, and `tracked.go`. Tests cover signatures, stale timestamps, DMs, bound threads, duplicate events, activity, refused hand-offs, cancel targets, and tracked delivery in `channel/slack`.