Slack

Let people talk to your agent in Slack channels, threads and DMs, and approve its actions with a button.

Add slack() to a host and each Slack conversation becomes a thread. The agent replies in place, and tool calls that need approval show Approve / Deny buttons.

Setup

Create a Slack app

In api.slack.com/apps, create an app and install it to your workspace. Give the bot token the chat:write scope, plus the *:history scopes for the conversations it joins (channels:history, groups:history, im:history). The history scopes let threads confirm whether a reply was already posted after a crash.

Set the secrets

export SLACK_SIGNING_SECRET=...   # Basic Information → Signing Secret
export SLACK_BOT_TOKEN=xoxb-...   # OAuth & Permissions → Bot User OAuth Token

Add the channel to your host

import { secret } from "@threads/core";
import { slack } from "@threads/slack";

const slackChannel = slack({
  agent: "support",
  signingSecret: secret("SLACK_SIGNING_SECRET"),
  botToken: secret("SLACK_BOT_TOKEN"),
  botUserId: "U0123456789",
});

Pass it to host({ channels: { slack: slackChannel } }). See Host server.

Point Slack at the webhook

Run threads dev and copy the printed URL, https://<your-host>/channels/slack/events. Use it as both the Event Subscriptions request URL (subscribe to message.channels, message.groups and message.im) and the Interactivity request URL. Slack's URL check is answered automatically.

Python: install the slack extra (uv sync --extra slack).

Options

agentstringrequired

The host agent key this channel routes to.

signingSecret / signing_secretSecretrequired

Verifies every request over its raw bytes, with a 5-minute replay window.

botToken / bot_tokenSecretrequired

Used to post replies.

botUserIdstring

TypeScript only. The bot's own user id, so its messages are ignored.

tenantstring | (teamId) => string | undefined

TypeScript only. The tenant for a workspace. Default slack:<team_id>. A function that returns undefined refuses that workspace.

What the agent sees

  • A message in a channel or DM starts or continues that conversation's thread. A reply inside a Slack thread continues that Slack thread's own conversation.
  • Bot messages, edits and other subtypes are ignored.
  • Each workspace is its own tenant (slack:<team_id>), so data from two workspaces never mixes. On Enterprise Grid the org and workspace together identify the installation.

Approvals

When a tool call needs approval, the reply carries Approve and Deny buttons. By default only the person who started the run can press them. To let others approve, list them on the agent:

const approving = agent({
  name: "support",
  model,
  approvers: [{ issuer: "slack:T024BE7LD", tenant: "slack:T024BE7LD", subject: "U0123456789" }],
});

T024BE7LD is the Slack team id and U0123456789 the user id. A press from anyone else changes nothing, and a second press of the same button is ignored. More in Human-in-the-loop.

Edit on GitHub

On this page