Slack Voice
@tuttiai/slack — give agents a bot token to read, post, and moderate messages in a workspace
The Slack voice gives agents a bot token they can use to read, post, and moderate messages in a workspace.
Write tools (post_message, update_message, delete_message, add_reaction, send_dm) are marked destructive: true, so HITL-enabled runtimes gate them behind human approval before anything hits the workspace.
Installation
npx tutti-ai add slack
Required permissions
permissions: ["network"]
Required environment variables
| Var | Description |
|---|---|
SLACK_BOT_TOKEN | Bot user OAuth token (starts with xoxb-) |
Add to your .env:
SLACK_BOT_TOKEN=xoxb-your-bot-token-here
App setup
- Create a Slack app at api.slack.com/apps → Create New App → From scratch.
- OAuth & Permissions → Bot Token Scopes. Recommended minimum:
chat:write— post messageschannels:read+channels:history— list and read public channelsgroups:read+groups:history— same for private channels (optional)reactions:write— add emoji reactionsusers:read— list membersteam:read— workspace metadataim:write— open DM channels (needed bysend_dm)
- Install to Workspace and approve the prompt.
- Copy the Bot User OAuth Token into
SLACK_BOT_TOKEN. - Invite the bot to each channel:
/invite @your-botfrom inside the channel.
Configuration
// Default: reads SLACK_BOT_TOKEN from env
new SlackVoice()
// Explicit token
new SlackVoice({ token: "xoxb-..." })
Tool reference
| Tool | Destructive | Description |
|---|---|---|
post_message | yes | Post to a channel. Optional thread_ts to reply in a thread. |
update_message | yes | Edit a message the bot wrote (Slack only allows bots to edit their own). |
delete_message | yes | Delete a message the bot posted. |
add_reaction | yes | React with an emoji name (with or without surrounding :). |
send_dm | yes | Open a DM and send a message in one step. |
list_messages | no | Recent messages, newest first, with limit / oldest / latest. |
get_message | no | Full detail on a message by channel + ts. |
list_channels | no | Public (and optionally private) channels with id, name, topic. |
list_members | no | Workspace members with handle, real name, bot/deleted flags. |
search_messages | no | Local substring search over the last 200 messages in a channel. |
get_workspace_info | no | Workspace name, domain, icon URL. |
Example
import { defineScore, AnthropicProvider } from "@tuttiai/core";
import { SlackVoice } from "@tuttiai/slack";
export default defineScore({
provider: new AnthropicProvider(),
agents: {
triage: {
name: "triage",
model: "claude-sonnet-4-20250514",
system_prompt:
"You triage incoming messages in #support. Read the channel, classify each message, and propose (but do not execute) a response unless explicitly approved.",
voices: [new SlackVoice()],
permissions: ["network"],
},
},
});
Run it:
tutti-ai run triage "Catch me up on #support since yesterday"
With a HITL-enabled runtime, any post_message / delete_message / send_dm call pauses for human approval before execution.
Inbound (inbox)
The same SlackClientWrapper powers @tuttiai/inbox’s Slack adapter. Inbound messages arrive over Socket Mode — no public webhook required. Two distinct tokens are needed:
- Bot user OAuth token (
xoxb-…) —SLACK_BOT_TOKEN. Used for outboundchat.postMessageand as the cache key for the shared wrapper. - App-level token (
xapp-…) —SLACK_APP_TOKEN. Created athttps://api.slack.com/apps/{your-app}/generalwith theconnections:writescope. Used only for the Socket Mode connection.
In your Slack app’s settings, enable Socket Mode and subscribe the bot to the events you want — message.channels and message.im cover the most common cases.
inbox: {
agent: "support",
adapters: [{ platform: "slack" }], // tokens come from env
}
The wrapper filters out bot_id events and any message with a non-default subtype (edits, channel-joins, …) before dispatch, so handlers only see fresh, human-authored messages. The voice’s outbound WebClient and the inbox’s Socket Mode connection share a single wrapper instance via SlackClientWrapper.forToken(botToken, factory?, { appToken }) — pay-for-what-you-use without duplicate auth plumbing.