Answer @mentions in Slack and Teams threads with the first streamed token under 2 s, keep thread context for follow-ups, acknowledge webhooks fast enough that Slack sends 0 retries in 50 test mentions, and handle 20 concurrent threads.
Recommended stack
- Chat SDK 4 (npm chat) + @chat-adapter/slack and @chat-adapter/teams - One handler set for both platforms. onNewMention, thread.subscribe, and thread.post work the same across Slack, Teams, Google Chat, Discord, and others.
- @chat-adapter/state-redis - Subscriptions and dedupe across instances. The memory adapter loses subscriptions on restart or with more than one instance.
- AI SDK 7 - Streaming model replies and tools. streamText output passes straight to thread.post as an async iterable.
- Next.js 16 route handlers - Webhook endpoints. after() lets the route acknowledge immediately and finish the reply in the background.
Build steps
- Create the Slack app with bot scopes (app_mentions:read, chat:write, channels:history, groups:history, im:history, reactions:write, assistant:write, users:read) and events pointed at /api/webhooks/slack; register the Teams bot for /api/webhooks/teams. Subscribe to bot events app_mention, message.channels, message.groups and message.im (follow-ups in subscribed threads need the message.* events).
- In lib/bot.ts create
new Chat({ userName, adapters: { slack: createSlackAdapter(), teams: createTeamsAdapter() }, state: createRedisState() }); credentials come from env vars. - bot.onNewMention: subscribe the thread, react to show it is working, load the last 20 thread messages as context, and stream the reply with thread.post(asyncIterable).
- bot.onSubscribedMessage: answer follow-ups in subscribed threads without a new mention; stop on "stop" or after 30 minutes idle.
- Webhook routes return
bot.webhooks.slack(request, { waitUntil: task => after(() => task) })(same for Teams). - Give the bot 2-3 read-only tools (search docs, look up a ticket); any write action posts a confirm card and runs only after a button click.
- Test locally with Slack socket mode: createSlackAdapter({ mode: "socket" }) with SLACK_APP_TOKEN (xapp-, connections:write), then deploy in webhook mode with REDIS_URL set (or @chat-adapter/state-pg with createPostgresState() and POSTGRES_URL).
Watch out for
- Doing model work before acknowledging the webhook makes Slack retry and the bot answers twice. Acknowledge first and dedupe by event id.
- Memory state in a multi-instance deploy drops subscriptions. Use Redis.
- Slack native streams expire after about 5 minutes; keep answers short and let the adapter start a new message.
- Replying to its own or other bots' messages creates loops. Ignore bot authors.
Definition of done
- An @mention in Slack streams a reply into the thread, and a follow-up without a mention gets a context-aware answer.
- The same works in a Teams channel.
- After restarting the app, the next message in a subscribed thread is still answered.
- 50 test mentions show 0 Slack retry deliveries in the logs.
- 20 concurrent mentions in different threads each get one reply; logs show no request with an X-Slack-Retry-Num header.