Files
vercel__chat/packages/adapter-slack
github-actions[bot] 470b6af94b chore(release): version packages (#748)
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @chat-adapter/slack@4.36.0

### Minor Changes

- 0153a39: Add `DateInput` and `NumberInput` modal children. The Slack
adapter renders them as a `datepicker` and a `number_input`, the Teams
adapter as `Input.Date` and `Input.Number`, and both submitted values
arrive in `event.values` as strings.

Teams submit values that arrive as JSON numbers are now stringified into
`event.values` instead of being dropped. This fixes `Input.Number`, but
applies to any numeric value a Teams dialog submits — a key that was
previously absent from `event.values` will now be present as a string.

### Patch Changes

- Updated dependencies [257a32d]
- Updated dependencies [c5d86b1]
- Updated dependencies [0153a39]
- Updated dependencies [b547f45]
- Updated dependencies [caa6325]
  - chat@4.36.0
  - @chat-adapter/shared@4.36.0
## @chat-adapter/teams@4.36.0

### Minor Changes

- 0153a39: Add `DateInput` and `NumberInput` modal children. The Slack
adapter renders them as a `datepicker` and a `number_input`, the Teams
adapter as `Input.Date` and `Input.Number`, and both submitted values
arrive in `event.values` as strings.

Teams submit values that arrive as JSON numbers are now stringified into
`event.values` instead of being dropped. This fixes `Input.Number`, but
applies to any numeric value a Teams dialog submits — a key that was
previously absent from `event.values` will now be present as a string.

### Patch Changes

- 257a32d: Route Teams personal and group conversations using their
explicit conversation type so group chats use buffered fallback even
when their IDs resemble direct messages.
- 3c37cfb: Authenticate connector-hosted inline attachments and parse
Teams file download cards.
- Updated dependencies [257a32d]
- Updated dependencies [c5d86b1]
- Updated dependencies [0153a39]
- Updated dependencies [b547f45]
- Updated dependencies [caa6325]
  - chat@4.36.0
  - @chat-adapter/shared@4.36.0
## @chat-adapter/x@4.36.0

### Minor Changes

- caa6325: Add XChat support to `@chat-adapter/x`, shipped from the new
`@chat-adapter/x/chat` subpath so it sits alongside the existing X
adapter. The XChat crypto stack (`@xdevplatform/chat-xdk`,
`@xdevplatform/xdk`, `juicebox-sdk`) is an optional peer dependency, so
existing `@chat-adapter/x` users are unaffected. All cryptography is
handled inside the adapter via `@xdevplatform/chat-xdk` (wasm) and all
REST goes through the typed `@xdevplatform/xdk` client. Only a bot token
and a Juicebox PIN are required: the bot's identity (user id and
@handle) is resolved from `GET /2/users/me` at startup.

- Encrypted send/receive in DMs and groups (webhook push + polling),
signature verification on by default; undecryptable or unverified events
are dropped
- Webhook POSTs must carry a valid `x-twitter-webhooks-signature`, which
X sends on every delivery. Set `consumerSecret` (or `X_CONSUMER_SECRET`)
to receive webhooks, or `disableWebhookVerification` when an upstream
layer already verifies them. Polling deployments are unaffected
- Mention detection from structured mention entities, swipe-replies to
the bot, and a plain-text `@handle` fallback; group replies sent as
quoted replies
- `openDM(userId)` starts (or reuses) an encrypted 1:1, running a full
key exchange when needed so the bot can message first
- Media both ways: inbound attachments with lazy download+decrypt,
outbound encrypted uploads
- Edit and delete of the bot's own messages; the first edit of a fresh
message is age-gated by `editSafetyDelayMs` (default 5000ms) so
receiving clients have stored the original
- Reactions in and out, read receipts (`sendReadReceipts`, default on),
typing keep-alive, configurable group welcome message
- Cards degrade to text with tappable URL/mention entities plus a URL
preview attachment
- Requests carry a `chat-sdk-xchat/<version>` User-Agent product token
so Chat SDK traffic is identifiable in X API request logs (a User-Agent
set via `apiHeaders` takes precedence)
- Registered in the `chat/adapters` catalog and the `create-chat-sdk`
CLI scaffold, with a new optional `importPath` catalog field for
adapters that ship on a subpath

### Patch Changes

- Updated dependencies [257a32d]
- Updated dependencies [c5d86b1]
- Updated dependencies [0153a39]
- Updated dependencies [b547f45]
- Updated dependencies [caa6325]
  - chat@4.36.0
  - @chat-adapter/shared@4.36.0
## chat@4.36.0

### Minor Changes

- c5d86b1: confine built-in agent read tools to the conversation being
handled, with an optional scope override
- 0153a39: Add `DateInput` and `NumberInput` modal children. The Slack
adapter renders them as a `datepicker` and a `number_input`, the Teams
adapter as `Input.Date` and `Input.Number`, and both submitted values
arrive in `event.values` as strings.

Teams submit values that arrive as JSON numbers are now stringified into
`event.values` instead of being dropped. This fixes `Input.Number`, but
applies to any numeric value a Teams dialog submits — a key that was
previously absent from `event.values` will now be present as a string.

### Patch Changes

- 257a32d: Route Teams personal and group conversations using their
explicit conversation type so group chats use buffered fallback even
when their IDs resemble direct messages.
- b547f45: Stop treating email addresses as bot mentions. A message
containing `jane@acme.com` no longer triggers a bot named `acme`,
because the `@` in `detectMention` must not follow a word character.
Real mentions are unaffected, including at the start of a message, after
punctuation, and suffixed names such as GitHub's `mybot[bot]`.
- caa6325: Add XChat support to `@chat-adapter/x`, shipped from the new
`@chat-adapter/x/chat` subpath so it sits alongside the existing X
adapter. The XChat crypto stack (`@xdevplatform/chat-xdk`,
`@xdevplatform/xdk`, `juicebox-sdk`) is an optional peer dependency, so
existing `@chat-adapter/x` users are unaffected. All cryptography is
handled inside the adapter via `@xdevplatform/chat-xdk` (wasm) and all
REST goes through the typed `@xdevplatform/xdk` client. Only a bot token
and a Juicebox PIN are required: the bot's identity (user id and
@handle) is resolved from `GET /2/users/me` at startup.

- Encrypted send/receive in DMs and groups (webhook push + polling),
signature verification on by default; undecryptable or unverified events
are dropped
- Webhook POSTs must carry a valid `x-twitter-webhooks-signature`, which
X sends on every delivery. Set `consumerSecret` (or `X_CONSUMER_SECRET`)
to receive webhooks, or `disableWebhookVerification` when an upstream
layer already verifies them. Polling deployments are unaffected
- Mention detection from structured mention entities, swipe-replies to
the bot, and a plain-text `@handle` fallback; group replies sent as
quoted replies
- `openDM(userId)` starts (or reuses) an encrypted 1:1, running a full
key exchange when needed so the bot can message first
- Media both ways: inbound attachments with lazy download+decrypt,
outbound encrypted uploads
- Edit and delete of the bot's own messages; the first edit of a fresh
message is age-gated by `editSafetyDelayMs` (default 5000ms) so
receiving clients have stored the original
- Reactions in and out, read receipts (`sendReadReceipts`, default on),
typing keep-alive, configurable group welcome message
- Cards degrade to text with tappable URL/mention entities plus a URL
preview attachment
- Requests carry a `chat-sdk-xchat/<version>` User-Agent product token
so Chat SDK traffic is identifiable in X API request logs (a User-Agent
set via `apiHeaders` takes precedence)
- Registered in the `chat/adapters` catalog and the `create-chat-sdk`
CLI scaffold, with a new optional `importPath` catalog field for
adapters that ship on a subpath
## @chat-adapter/discord@4.36.0

### Patch Changes

- Updated dependencies [257a32d]
- Updated dependencies [c5d86b1]
- Updated dependencies [0153a39]
- Updated dependencies [b547f45]
- Updated dependencies [caa6325]
  - chat@4.36.0
  - @chat-adapter/shared@4.36.0
## @chat-adapter/gchat@4.36.0

### Patch Changes

- Updated dependencies [257a32d]
- Updated dependencies [c5d86b1]
- Updated dependencies [0153a39]
- Updated dependencies [b547f45]
- Updated dependencies [caa6325]
  - chat@4.36.0
  - @chat-adapter/shared@4.36.0
## @chat-adapter/github@4.36.0

### Patch Changes

- Updated dependencies [257a32d]
- Updated dependencies [c5d86b1]
- Updated dependencies [0153a39]
- Updated dependencies [b547f45]
- Updated dependencies [caa6325]
  - chat@4.36.0
  - @chat-adapter/shared@4.36.0
## @chat-adapter/linear@4.36.0

### Patch Changes

- Updated dependencies [257a32d]
- Updated dependencies [c5d86b1]
- Updated dependencies [0153a39]
- Updated dependencies [b547f45]
- Updated dependencies [caa6325]
  - chat@4.36.0
  - @chat-adapter/shared@4.36.0
## @chat-adapter/messenger@4.36.0

### Patch Changes

- Updated dependencies [257a32d]
- Updated dependencies [c5d86b1]
- Updated dependencies [0153a39]
- Updated dependencies [b547f45]
- Updated dependencies [caa6325]
  - chat@4.36.0
  - @chat-adapter/shared@4.36.0
## @chat-adapter/shared@4.36.0

### Patch Changes

- Updated dependencies [257a32d]
- Updated dependencies [c5d86b1]
- Updated dependencies [0153a39]
- Updated dependencies [b547f45]
- Updated dependencies [caa6325]
  - chat@4.36.0
## @chat-adapter/telegram@4.36.0

### Patch Changes

- 53bf73d: Preserve Telegram stable media identifiers in normalized
attachment metadata and report photo attachments as JPEG.
- Updated dependencies [257a32d]
- Updated dependencies [c5d86b1]
- Updated dependencies [0153a39]
- Updated dependencies [b547f45]
- Updated dependencies [caa6325]
  - chat@4.36.0
  - @chat-adapter/shared@4.36.0
## @chat-adapter/twilio@4.36.0

### Patch Changes

- Updated dependencies [257a32d]
- Updated dependencies [c5d86b1]
- Updated dependencies [0153a39]
- Updated dependencies [b547f45]
- Updated dependencies [caa6325]
  - chat@4.36.0
  - @chat-adapter/shared@4.36.0
## @chat-adapter/web@4.36.0

### Patch Changes

- Updated dependencies [257a32d]
- Updated dependencies [c5d86b1]
- Updated dependencies [0153a39]
- Updated dependencies [b547f45]
- Updated dependencies [caa6325]
  - chat@4.36.0
  - @chat-adapter/shared@4.36.0
## @chat-adapter/whatsapp@4.36.0

### Patch Changes

- Updated dependencies [257a32d]
- Updated dependencies [c5d86b1]
- Updated dependencies [0153a39]
- Updated dependencies [b547f45]
- Updated dependencies [caa6325]
  - chat@4.36.0
  - @chat-adapter/shared@4.36.0
## create-chat-sdk@0.2.1

### Patch Changes

- caa6325: Add XChat support to `@chat-adapter/x`, shipped from the new
`@chat-adapter/x/chat` subpath so it sits alongside the existing X
adapter. The XChat crypto stack (`@xdevplatform/chat-xdk`,
`@xdevplatform/xdk`, `juicebox-sdk`) is an optional peer dependency, so
existing `@chat-adapter/x` users are unaffected. All cryptography is
handled inside the adapter via `@xdevplatform/chat-xdk` (wasm) and all
REST goes through the typed `@xdevplatform/xdk` client. Only a bot token
and a Juicebox PIN are required: the bot's identity (user id and
@handle) is resolved from `GET /2/users/me` at startup.

- Encrypted send/receive in DMs and groups (webhook push + polling),
signature verification on by default; undecryptable or unverified events
are dropped
- Webhook POSTs must carry a valid `x-twitter-webhooks-signature`, which
X sends on every delivery. Set `consumerSecret` (or `X_CONSUMER_SECRET`)
to receive webhooks, or `disableWebhookVerification` when an upstream
layer already verifies them. Polling deployments are unaffected
- Mention detection from structured mention entities, swipe-replies to
the bot, and a plain-text `@handle` fallback; group replies sent as
quoted replies
- `openDM(userId)` starts (or reuses) an encrypted 1:1, running a full
key exchange when needed so the bot can message first
- Media both ways: inbound attachments with lazy download+decrypt,
outbound encrypted uploads
- Edit and delete of the bot's own messages; the first edit of a fresh
message is age-gated by `editSafetyDelayMs` (default 5000ms) so
receiving clients have stored the original
- Reactions in and out, read receipts (`sendReadReceipts`, default on),
typing keep-alive, configurable group welcome message
- Cards degrade to text with tappable URL/mention entities plus a URL
preview attachment
- Requests carry a `chat-sdk-xchat/<version>` User-Agent product token
so Chat SDK traffic is identifiable in X API request logs (a User-Agent
set via `apiHeaders` takes precedence)
- Registered in the `chat/adapters` catalog and the `create-chat-sdk`
CLI scaffold, with a new optional `importPath` catalog field for
adapters that ship on a subpath
## @chat-adapter/state-ioredis@4.36.0

### Patch Changes

- Updated dependencies [257a32d]
- Updated dependencies [c5d86b1]
- Updated dependencies [0153a39]
- Updated dependencies [b547f45]
- Updated dependencies [caa6325]
  - chat@4.36.0
## @chat-adapter/state-memory@4.36.0

### Patch Changes

- Updated dependencies [257a32d]
- Updated dependencies [c5d86b1]
- Updated dependencies [0153a39]
- Updated dependencies [b547f45]
- Updated dependencies [caa6325]
  - chat@4.36.0
## @chat-adapter/state-pg@4.36.0

### Patch Changes

- Updated dependencies [257a32d]
- Updated dependencies [c5d86b1]
- Updated dependencies [0153a39]
- Updated dependencies [b547f45]
- Updated dependencies [caa6325]
  - chat@4.36.0
## @chat-adapter/state-redis@4.36.0

### Patch Changes

- Updated dependencies [257a32d]
- Updated dependencies [c5d86b1]
- Updated dependencies [0153a39]
- Updated dependencies [b547f45]
- Updated dependencies [caa6325]
  - chat@4.36.0
## @chat-adapter/tests@4.36.0

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-01 01:19:44 +01:00
..

Slack adapter for Chat SDK

@chat-adapter/slack

npm package: @chat-adapter/slack

Agent Stack MIT License

Slack adapter for Chat SDK. Configure single-workspace or multi-workspace OAuth deployments.

Documentation: chat-sdk.dev/adapters/official/slack · Guides: vercel.com/kb/chat-sdk

Installation

pnpm add @chat-adapter/slack

Scaffold with the CLI

To scaffold a new Slack bot with this adapter preselected:

npx create-chat-sdk@latest my-bot --adapter slack memory

Visit the adapters directory to see other available official and vendor-official adapters.

Single-workspace mode

For bots deployed to a single Slack workspace. The adapter auto-detects SLACK_BOT_TOKEN and SLACK_SIGNING_SECRET from environment variables:

import { Chat } from "chat";
import { createSlackAdapter } from "@chat-adapter/slack";

const bot = new Chat({
  userName: "mybot",
  adapters: {
    slack: createSlackAdapter(),
  },
});

bot.onNewMention(async (thread, message) => {
  await thread.post("Hello from Slack!");
});

Token rotation

botToken accepts a function returning a string or Promise<string> — the resolver is invoked per API call, so it composes with Slack token rotation (12-hour TTL) or lazy fetch from a secret manager:

createSlackAdapter({
  botToken: async () => await secrets.get("slack-bot-token"),
});

If the resolver is expensive (e.g. a vault round-trip), implement caching inside the resolver itself.

Custom webhook verification

Pass webhookVerifier to replace the built-in HMAC check — useful when verification runs in a proxy or signing layer ahead of your handler:

createSlackAdapter({
  webhookVerifier: async (request, body) => {
    if (!(await myProxy.verify(request))) {
      throw new Error("invalid");
    }
    return true; // or return a string to substitute the verified body
  },
});

If both signingSecret and webhookVerifier are set, webhookVerifier wins — it also takes precedence over the SLACK_SIGNING_SECRET env var, so an env-configured deployment can't silently shadow a verifier you wired up. When using webhookVerifier, you are responsible for replay/timestamp protection — the built-in 5-minute timestamp tolerance only applies to the signingSecret path.

Multi-workspace mode

For apps installed across multiple Slack workspaces via OAuth, omit botToken and provide OAuth credentials instead. The adapter resolves tokens dynamically from your state adapter using the team_id from incoming webhooks — or enterprise_id for Enterprise Grid org-wide installs (is_enterprise_install: true).

When you pass any auth-related config (like clientId), the adapter won't fall back to env vars for other auth fields, preventing accidental mixing of auth modes.

import { createSlackAdapter } from "@chat-adapter/slack";
import { createRedisState } from "@chat-adapter/state-redis";

const slackAdapter = createSlackAdapter({
  clientId: process.env.SLACK_CLIENT_ID!,
  clientSecret: process.env.SLACK_CLIENT_SECRET!,
});

const bot = new Chat({
  userName: "mybot",
  adapters: { slack: slackAdapter },
  state: createRedisState(),
});

OAuth callback

The adapter handles the full Slack OAuth V2 exchange. Point your OAuth redirect URL to a route that calls handleOAuthCallback:

import { slackAdapter } from "@/lib/bot";

export async function GET(request: Request) {
  const { teamId } = await slackAdapter.handleOAuthCallback(request, {
    redirectUri: process.env.SLACK_REDIRECT_URI,
  });
  return new Response(`Installed for team ${teamId}!`);
}

If your install flow uses a specific redirect URI, pass the same value here that you used during the authorize step. This is especially useful when one app supports multiple redirect URLs. When no option is provided, the adapter still falls back to redirect_uri on the callback request URL.

For Enterprise Grid org-wide installs (is_enterprise_install), Slack returns no team and the installation is keyed by the enterprise ID instead. The returned teamId is always the storage key — the enterprise ID for org-wide installs — so it round-trips with getInstallation and deleteInstallation for both install types. The result also includes enterpriseId and isEnterpriseInstall when you need to distinguish them.

Using the adapter outside webhooks

During webhook handling, the adapter resolves tokens automatically from team_id. Outside that context (e.g. cron jobs or background workers), use getInstallation and withBotToken:

const install = await slackAdapter.getInstallation(teamId);
if (!install) throw new Error("Workspace not installed");

await slackAdapter.withBotToken(
  install.botToken,
  async () => {
    const thread = bot.thread("slack:C12345:1234567890.123456");
    await thread.post("Hello from a cron job!");
  },
  { installationId: teamId }
);

withBotToken uses AsyncLocalStorage under the hood, so concurrent calls with different tokens are isolated. In multi-workspace deployments, pass installationId (the team_id, or enterprise_id for org-wide installs) so per-user caches are scoped to that installation and don't bleed across tenants.

Removing installations

await slackAdapter.deleteInstallation(teamId);

Token encryption

Pass a base64-encoded 32-byte key as encryptionKey to encrypt bot tokens at rest using AES-256-GCM:

openssl rand -base64 32

When encryptionKey is set, setInstallation() encrypts the token before storing and getInstallation() decrypts it transparently.

Vercel Connect

Use Vercel Connect to source the bot token at runtime instead of storing one. The connectSlackAdapter() helper from @vercel/connect/chat wires both a botToken resolver and a webhookVerifier for Connect trigger-forwarded webhooks:

import { createSlackAdapter } from "@chat-adapter/slack";
import { connectSlackAdapter } from "@vercel/connect/chat";

createSlackAdapter({
  ...connectSlackAdapter("slack/acme-slack"),
});

This is equivalent to passing a botToken resolver that calls getToken and a webhookVerifier that validates the Vercel OIDC token Connect attaches. Omit signingSecret / SLACK_SIGNING_SECRET when using it.

External installation provider

For deployments that manage Slack tokens in an external system (e.g. Vercel Connect), pass installationProvider to bypass the internal state adapter when resolving tokens for incoming webhooks:

createSlackAdapter({
  clientId: process.env.SLACK_CLIENT_ID!,
  clientSecret: process.env.SLACK_CLIENT_SECRET!,
  installationProvider: {
    getInstallation: async (installationId, isEnterpriseInstall) => {
      // installationId is enterprise_id when isEnterpriseInstall is true,
      // otherwise team_id. Return null if not found.
      return await myTokenStore.lookup(installationId, isEnterpriseInstall);
    },
  },
});

When configured, the provider's getInstallation is called for every webhook event, slash command, and interactive payload. It is read-only — the adapter's setInstallation, deleteInstallation, and handleOAuthCallback continue to write to the internal state adapter, so callers using a provider should manage their own writes through their external system.

Enterprise Grid

For Enterprise Grid org-wide installs the adapter handles the Grid-specific mechanics automatically:

  • Installations are keyed by enterprise_id and incoming payloads resolve tokens the same way (is_enterprise_install).
  • API calls made while handling an event pass the event's team_id explicitly — org-wide tokens span every workspace in the org, and Slack requires the ID on workspace-scoped methods.
  • When an event arrives from a shared channel hosted on another workspace (context_team_id), channel-addressed calls echo it back as client_context_team_id.
  • Retried event deliveries are deduplicated by event_id via the state adapter, so Slack's redelivery of slow-acked events doesn't double-process reactions or assistant events.
  • User profile caches and mention resolution are scoped per installation, so same-named users in different workspaces never cross-resolve.

Socket mode

For environments behind firewalls that can't expose public HTTP endpoints, the adapter supports Slack Socket Mode. Instead of receiving webhooks, the adapter connects to Slack over a WebSocket.

import { Chat } from "chat";
import { createSlackAdapter } from "@chat-adapter/slack";

const bot = new Chat({
  userName: "mybot",
  adapters: {
    slack: createSlackAdapter({
      mode: "socket",
      appToken: process.env.SLACK_APP_TOKEN!,
      botToken: process.env.SLACK_BOT_TOKEN!,
    }),
  },
});

Slack app setup for socket mode

  1. Go to your app's settings at api.slack.com/apps
  2. Navigate to Socket Mode and enable it
  3. Generate an App-Level Token with the connections:write scope — this is your SLACK_APP_TOKEN (xapp-...)
  4. Event subscriptions and interactivity still need to be configured, but no public request URL is required

Socket mode works with both single-workspace tokens and multi-workspace OAuth: events arriving over the socket (or forwarded from a socket listener) resolve per-installation tokens by team_id — or enterprise_id for Enterprise Grid org-wide installs — the same way the webhook path does.

Socket mode on serverless (Vercel)

Socket mode requires a persistent WebSocket connection, which doesn't fit the request/response model of serverless functions. The adapter provides a forwarding mechanism to bridge this gap:

  1. A cron job periodically starts a transient socket listener
  2. The listener connects via WebSocket, acks events immediately, and forwards them as HTTP requests to your webhook endpoint
  3. Your existing webhook route processes the forwarded events normally
// api/slack/socket-mode/route.ts
import { after } from "next/server";
import { bot } from "@/lib/bot";

export const maxDuration = 800;

export async function GET(request: Request) {
  const authHeader = request.headers.get("authorization");
  if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
    return new Response("Unauthorized", { status: 401 });
  }

  await bot.initialize();

  const slack = bot.getAdapter("slack");
  const webhookUrl = `https://${process.env.VERCEL_URL}/api/webhooks/slack`;

  return slack.startSocketModeListener(
    { waitUntil: (task: Promise<unknown>) => after(() => task) },
    600_000, // 10 minutes
    undefined,
    webhookUrl
  );
}

Schedule the cron job to run every 9 minutes (overlapping with the 10-minute listener duration) to maintain continuous coverage:

// vercel.json
{
  "crons": [
    {
      "path": "/api/slack/socket-mode",
      "schedule": "*/9 * * * *"
    }
  ]
}

Forwarded events are authenticated using the socketForwardingSecret config option (defaults to SLACK_SOCKET_FORWARDING_SECRET env var, falling back to appToken).

Slack app setup

1. Create a Slack app from manifest

  1. Go to api.slack.com/apps
  2. Click Create New App then From an app manifest
  3. Select your workspace and paste the following manifest:
display_information:
  name: My Bot
  description: A bot built with chat-sdk

features:
  bot_user:
    display_name: My Bot
    always_online: true

oauth_config:
  scopes:
    bot:
      - app_mentions:read
      - channels:history
      - channels:read
      - chat:write
      - groups:history
      - groups:read
      - im:history
      - im:read
      - mpim:history
      - mpim:read
      - reactions:read
      - reactions:write
      - users:read

settings:
  event_subscriptions:
    request_url: https://your-domain.com/api/webhooks/slack
    bot_events:
      - app_mention
      - message.channels
      - message.groups
      - message.im
      - message.mpim
      - member_joined_channel
      - assistant_thread_started
      - assistant_thread_context_changed
  interactivity:
    is_enabled: true
    request_url: https://your-domain.com/api/webhooks/slack
  org_deploy_enabled: false
  socket_mode_enabled: false
  token_rotation_enabled: false
  1. Replace https://your-domain.com/api/webhooks/slack with your deployed webhook URL
  2. Click Create

2. Get credentials

After creating the app, go to Basic Information → App Credentials and copy:

  • Signing Secret as SLACK_SIGNING_SECRET
  • Client ID as SLACK_CLIENT_ID (multi-workspace only)
  • Client Secret as SLACK_CLIENT_SECRET (multi-workspace only)

Single workspace: Go to OAuth & Permissions, click Install to Workspace, and copy the Bot User OAuth Token (xoxb-...) as SLACK_BOT_TOKEN.

Multi-workspace: Enable Manage Distribution under Basic Information and set up an OAuth redirect URL pointing to your callback route.

3. Configure slash commands (optional)

  1. Go to Slash Commands in your app settings
  2. Click Create New Command
  3. Set Command (e.g., /feedback)
  4. Set Request URL to https://your-domain.com/api/webhooks/slack
  5. Add a description and click Save

Configuration

All options are auto-detected from environment variables when not provided. You can call createSlackAdapter() with no arguments if the env vars are set.

Option Required Description
botToken No Bot token (xoxb-...) or a function returning one (sync or async) for rotation/lazy fetch. Auto-detected from SLACK_BOT_TOKEN
signingSecret No* Signing secret for webhook verification. Auto-detected from SLACK_SIGNING_SECRET
webhookVerifier No* Custom verifier (request, body) => unknown | Promise<unknown> used in place of signingSecret. Returning a string substitutes the verified body for downstream parsing
mode No Connection mode: "webhook" (default) or "socket"
appToken No** App-level token (xapp-...) for socket mode. Auto-detected from SLACK_APP_TOKEN
socketForwardingSecret No Shared secret for authenticating forwarded socket events. Auto-detected from SLACK_SOCKET_FORWARDING_SECRET, falls back to appToken
clientId No App client ID for multi-workspace OAuth. Auto-detected from SLACK_CLIENT_ID
clientSecret No App client secret for multi-workspace OAuth. Auto-detected from SLACK_CLIENT_SECRET
encryptionKey No AES-256-GCM key for encrypting stored tokens. Auto-detected from SLACK_ENCRYPTION_KEY
installationKeyPrefix No Prefix for the state key used to store workspace installations. Defaults to slack:installation. The full key is {prefix}:{teamId} (or {prefix}:{enterpriseId} for Enterprise Grid org-wide installs)
installationProvider No External installation lookup { getInstallation(installationId, isEnterpriseInstall) => Promise<SlackInstallation | null> }. When set, bypasses the internal state adapter for token resolution on incoming webhooks. Read-only — manage your own writes externally
apiUrl No Override the Slack Web API base URL (e.g. for GovSlack or a self-hosted gateway). Auto-detected from SLACK_API_URL
webClientOptions No Options forwarded to Slack WebClient instances, excluding slackApiUrl. Supports settings such as retryConfig, per-request timeout, and rejectRateLimitedCalls
logger No Logger instance (defaults to ConsoleLogger("info"))

*signingSecret is required for webhook mode — either via config, SLACK_SIGNING_SECRET env var, or a webhookVerifier. **appToken is required for socket mode — either via config or SLACK_APP_TOKEN env var.

Environment variables

SLACK_BOT_TOKEN=xoxb-...             # Single-workspace only
SLACK_SIGNING_SECRET=...             # Required for webhook mode
SLACK_APP_TOKEN=xapp-...             # Required for socket mode
SLACK_SOCKET_FORWARDING_SECRET=...   # Optional, for socket event forwarding auth
SLACK_CLIENT_ID=...                  # Multi-workspace only
SLACK_CLIENT_SECRET=...              # Multi-workspace only
SLACK_ENCRYPTION_KEY=...             # Optional, for token encryption
SLACK_API_URL=...                    # Optional, for GovSlack or a self-hosted gateway

Features

Messaging

Feature Supported
Post message Yes
Edit message Yes
Delete message Yes
File uploads Yes
Streaming Native API
Scheduled messages Yes (native, with cancel)

Rich content

Feature Supported
Card format Block Kit
Buttons Yes
Link buttons Yes
Select menus Yes
Tables Block Kit data table (paginated, sortable)
Charts Block Kit data visualization (pie, bar, area, line)
Fields Yes
Images in cards Yes
Modals Yes

Conversations

Feature Supported
Slash commands Yes
Mentions Yes
Add reactions Yes
Remove reactions Yes
Typing indicator Yes
DMs Yes
Ephemeral messages Yes (native)

Message history

Feature Supported
Fetch messages Yes
Fetch single message Yes
Fetch thread info Yes
Fetch channel messages Yes
List threads Yes
Fetch channel info Yes
Post channel message Yes

Author emails

Incoming message authors include message.author.email when your app has the users:read.email scope (add it to the manifest alongside users:read). Without the scope, or when Slack omits the profile email, the field is undefined. The email comes from the same cached users.info lookup the adapter already performs for display names, so no extra API call is made per message.

Platform-specific

Feature Supported
Assistants API Yes
Member joined channel Yes
App Home tab Yes

Direct WebClient access

Use adapter.webClient to get a typed WebClient from @slack/web-api for any Web API call that isn't wrapped by the SDK's high-level methods.

import type { SlackAdapter } from "@chat-adapter/slack";

bot.onAction("pin-this", async (event) => {
  const slack = bot.getAdapter("slack") as SlackAdapter;
  await slack.webClient.pins.add({
    channel: event.thread!.channel.id.replace(/^slack:/, ""),
    timestamp: event.messageId,
  });
});

The returned client is bound to the bot token resolved in this order:

  1. The token from the current request context — set automatically during webhook handling, or by adapter.withBotToken(token, fn).
  2. The default botToken, when configured as a static string or a synchronous resolver function.

adapter.webClient throws AuthenticationError outside of any context in multi-workspace mode, or when botToken is configured as an async resolver function. For both cases, await the token first and bind it explicitly:

const install = await slackAdapter.getInstallation(teamId);
if (!install) throw new Error("Workspace not installed");

await slackAdapter.withBotToken(install.botToken, async () => {
  const me = await slackAdapter.webClient.auth.test();
  console.log("Bot user:", me.user_id);
});

The previous .client getter still works as a deprecated alias for .webClient.

Internal API calls (postMessage, editMessage, fetchMessages, etc.) are unaffected — they continue to resolve tokens through the same async path they always have.

Slack Assistants API

The adapter supports Slack's Assistants API for building AI-powered assistant experiences. This enables suggested prompts, status indicators, and thread titles in assistant DM threads.

Event handlers

Register handlers on the Chat instance:

bot.onAssistantThreadStarted(async (event) => {
  const slack = bot.getAdapter("slack") as SlackAdapter;
  await slack.setSuggestedPrompts(event.channelId, event.threadTs, [
    { title: "Summarize", message: "Summarize this channel" },
    { title: "Draft", message: "Help me draft a message" },
  ]);
});

bot.onAssistantContextChanged(async (event) => {
  // User navigated to a different channel with the assistant panel open
});

Adapter methods

The SlackAdapter exposes these methods for the Assistants API:

Method Description
setSuggestedPrompts(channelId, threadTs, prompts, title?) Show prompt suggestions in the thread
setAssistantStatus(channelId, threadTs, status) Show a thinking/status indicator
setAssistantTitle(channelId, threadTs, title) Set the thread title (shown in History)
publishHomeView(userId, view) Publish a Home tab view for a user
startTyping(threadId, status) Show a custom loading status (requires assistant:write scope)

Required scopes and events

Add these to your Slack app manifest for Assistants API support:

oauth_config:
  scopes:
    bot:
      - assistant:write

settings:
  event_subscriptions:
    bot_events:
      - assistant_thread_started
      - assistant_thread_context_changed

Stream with stop blocks

When streaming in an assistant thread, attach Block Kit elements to the final message by wrapping the stream in a StreamingPlan and passing endWith:

import { StreamingPlan } from "chat";

await thread.post(
  new StreamingPlan(textStream, {
    endWith: [
      { type: "actions", elements: [{ type: "button", text: { type: "plain_text", text: "Retry" }, action_id: "retry" }] },
    ],
  })
);

Troubleshooting

handleOAuthCallback throws "Adapter not initialized"

  • Call await bot.initialize() before handleOAuthCallback() in your callback route.
  • In a Next.js app, this ensures:
    • state adapter is connected
    • the Slack adapter is attached to Chat
    • installation writes succeed
const slackAdapter = bot.getAdapter("slack");

await bot.initialize();
await slackAdapter.handleOAuthCallback(request);

"Invalid signature" error

  • Verify SLACK_SIGNING_SECRET is correct
  • Check that the request timestamp is within 5 minutes (clock sync issue)
  • If using a custom webhookVerifier, the error also surfaces when the verifier throws or returns a falsy value

Bot not responding to messages

  • Verify event subscriptions are configured
  • Check that the bot has been added to the channel
  • Ensure the webhook URL is correct and accessible

Resources

See all guides and templates at chat-sdk.dev/resources.

AI Coding Agents

If you use an AI coding agent such as OpenAI Codex, Claude Code, or Cursor, install the Chat SDK skill so it knows the SDK APIs, adapter patterns, and project conventions before writing code.

npx skills add vercel/chat

The skill references bundled documentation in node_modules/chat/docs, plus adapter guides and starter templates in the published package.

You can also install the Vercel Plugin for a broader agent toolkit — it includes the Chat SDK skill alongside specialist agents, agent slash commands, and more:

npx plugins add vercel/vercel-plugin

The plugin is optional; the skill alone is enough to build with Chat SDK.

For agent-readable documentation, see chat-sdk.dev/llms.txt (page index) or chat-sdk.dev/llms-full.txt (full text).

License

MIT