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

VarDescription
SLACK_BOT_TOKENBot user OAuth token (starts with xoxb-)

Add to your .env:

SLACK_BOT_TOKEN=xoxb-your-bot-token-here

App setup

  1. Create a Slack app at api.slack.com/appsCreate New AppFrom scratch.
  2. OAuth & PermissionsBot Token Scopes. Recommended minimum:
    • chat:write — post messages
    • channels:read + channels:history — list and read public channels
    • groups:read + groups:history — same for private channels (optional)
    • reactions:write — add emoji reactions
    • users:read — list members
    • team:read — workspace metadata
    • im:write — open DM channels (needed by send_dm)
  3. Install to Workspace and approve the prompt.
  4. Copy the Bot User OAuth Token into SLACK_BOT_TOKEN.
  5. Invite the bot to each channel: /invite @your-bot from inside the channel.

Configuration

// Default: reads SLACK_BOT_TOKEN from env
new SlackVoice()

// Explicit token
new SlackVoice({ token: "xoxb-..." })

Tool reference

ToolDestructiveDescription
post_messageyesPost to a channel. Optional thread_ts to reply in a thread.
update_messageyesEdit a message the bot wrote (Slack only allows bots to edit their own).
delete_messageyesDelete a message the bot posted.
add_reactionyesReact with an emoji name (with or without surrounding :).
send_dmyesOpen a DM and send a message in one step.
list_messagesnoRecent messages, newest first, with limit / oldest / latest.
get_messagenoFull detail on a message by channel + ts.
list_channelsnoPublic (and optionally private) channels with id, name, topic.
list_membersnoWorkspace members with handle, real name, bot/deleted flags.
search_messagesnoLocal substring search over the last 200 messages in a channel.
get_workspace_infonoWorkspace 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 outbound chat.postMessage and as the cache key for the shared wrapper.
  • App-level token (xapp-…) — SLACK_APP_TOKEN. Created at https://api.slack.com/apps/{your-app}/general with the connections:write scope. 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.

Edit this page on GitHub →