Skip to main content

Slackbot with AI SDK

Learn how to build a Slack bot powered by the AI SDK that can respond to direct messages and mentions with tool-calling capabilities.

Prerequisites

  • Node.js 18+
  • A Slack workspace where you can install apps
  • A Vercel account for deployment
  • Vercel AI Gateway API key

Slack App Setup

  1. Go to api.slack.com/apps
  2. Click “Create New App” and choose “From scratch”
  3. Give your app a name and select your workspace
  4. Under “OAuth & Permissions”, add bot token scopes:
    • app_mentions:read
    • chat:write
    • im:history
    • im:write
    • assistant:write
  5. Install the app to your workspace
  6. Copy the Bot User OAuth Token and Signing Secret

Project Setup

Clone the starter repository:
git clone https://github.com/vercel-labs/ai-sdk-slackbot.git
cd ai-sdk-slackbot
git checkout starter
pnpm install
Create a .env file:
SLACK_BOT_TOKEN=your_slack_bot_token
SLACK_SIGNING_SECRET=your_slack_signing_secret
AI_GATEWAY_API_KEY=your_ai_gateway_key
EXA_API_KEY=your_exa_api_key

Implementation

Event Handler

Create an API route to handle Slack events:
import type { SlackEvent } from '@slack/web-api';
import {
  assistantThreadMessage,
  handleNewAssistantMessage,
} from '../lib/handle-messages';
import { waitUntil } from '@vercel/functions';
import { handleNewAppMention } from '../lib/handle-app-mention';
import { verifyRequest, getBotId } from '../lib/slack-utils';

export async function POST(request: Request) {
  const rawBody = await request.text();
  const payload = JSON.parse(rawBody);
  const requestType = payload.type as 'url_verification' | 'event_callback';

  // Handle URL verification
  if (requestType === 'url_verification') {
    return new Response(payload.challenge, { status: 200 });
  }

  await verifyRequest({ requestType, request, rawBody });

  try {
    const botUserId = await getBotId();
    const event = payload.event as SlackEvent;

    if (event.type === 'app_mention') {
      waitUntil(handleNewAppMention(event, botUserId));
    }

    if (event.type === 'assistant_thread_started') {
      waitUntil(assistantThreadMessage(event));
    }

    if (
      event.type === 'message' &&
      !event.subtype &&
      event.channel_type === 'im' &&
      !event.bot_id
    ) {
      waitUntil(handleNewAssistantMessage(event, botUserId));
    }

    return new Response('Success!', { status: 200 });
  } catch (error) {
    console.error('Error generating response', error);
    return new Response('Error generating response', { status: 500 });
  }
}

Generate AI Responses

Create the core AI logic with tools:
import { generateText, tool, ModelMessage, stepCountIs } from 'ai';
import { z } from 'zod';
import { exa } from './utils';

export const generateResponse = async (
  messages: ModelMessage[],
  updateStatus?: (status: string) => void,
) => {
  const { text } = await generateText({
    model: 'anthropic/claude-sonnet-4-20250514',
    system: `You are a Slack bot assistant. Keep your responses concise and to the point.
    - Do not tag users.
    - Current date is: ${new Date().toISOString().split('T')[0]}
    - Always include sources in your final response if you use web search.`,
    messages,
    stopWhen: stepCountIs(10),
    tools: {
      getWeather: tool({
        description: 'Get the current weather at a location',
        inputSchema: z.object({
          latitude: z.number(),
          longitude: z.number(),
          city: z.string(),
        }),
        execute: async ({ latitude, longitude, city }) => {
          updateStatus?.(`is getting weather for ${city}...`);

          const response = await fetch(
            `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&current=temperature_2m,weathercode,relativehumidity_2m&timezone=auto`,
          );

          const weatherData = await response.json();
          return {
            temperature: weatherData.current.temperature_2m,
            weatherCode: weatherData.current.weathercode,
            humidity: weatherData.current.relativehumidity_2m,
            city,
          };
        },
      }),
      searchWeb: tool({
        description: 'Use this to search the web for information',
        inputSchema: z.object({
          query: z.string(),
          specificDomain: z
            .string()
            .nullable()
            .describe(
              'a domain to search if the user specifies e.g. bbc.com',
            ),
        }),
        execute: async ({ query, specificDomain }) => {
          updateStatus?.(`is searching the web for ${query}...`);
          const { results } = await exa.searchAndContents(query, {
            livecrawl: 'always',
            numResults: 3,
            includeDomains: specificDomain ? [specificDomain] : undefined,
          });

          return {
            results: results.map(result => ({
              title: result.title,
              url: result.url,
              snippet: result.text.slice(0, 1000),
            })),
          };
        },
      }),
    },
  });

  // Convert markdown to Slack mrkdwn format
  return text.replace(/\[(.*?)\]\((.*?)\)/g, '<$2|$1>').replace(/\*\*/g, '*');
};

Handle App Mentions

Process mentions in channels:
import { AppMentionEvent } from '@slack/web-api';
import { client, getThread } from './slack-utils';
import { generateResponse } from './generate-response';

export async function handleNewAppMention(
  event: AppMentionEvent,
  botUserId: string,
) {
  if (event.bot_id || event.bot_id === botUserId || event.bot_profile) {
    return;
  }

  const { thread_ts, channel } = event;
  
  // Post initial "thinking" message
  const initialMessage = await client.chat.postMessage({
    channel: event.channel,
    thread_ts: event.thread_ts ?? event.ts,
    text: 'is thinking...',
  });

  const updateMessage = async (status: string) => {
    await client.chat.update({
      channel: event.channel,
      ts: initialMessage.ts as string,
      text: status,
    });
  };

  if (thread_ts) {
    const messages = await getThread(channel, thread_ts, botUserId);
    const result = await generateResponse(messages, updateMessage);
    updateMessage(result);
  } else {
    const result = await generateResponse(
      [{ role: 'user', content: event.text }],
      updateMessage,
    );
    updateMessage(result);
  }
}

Handle Direct Messages

Process DMs to the bot:
import type { GenericMessageEvent } from '@slack/web-api';
import { client, getThread } from './slack-utils';
import { generateResponse } from './generate-response';

export async function handleNewAssistantMessage(
  event: GenericMessageEvent,
  botUserId: string,
) {
  if (
    event.bot_id ||
    event.bot_id === botUserId ||
    event.bot_profile ||
    !event.thread_ts
  )
    return;

  const { thread_ts, channel } = event;
  
  const messages = await getThread(channel, thread_ts, botUserId);
  const result = await generateResponse(messages);

  await client.chat.postMessage({
    channel: channel,
    thread_ts: thread_ts,
    text: result,
    unfurl_links: false,
  });
}

Key Concepts

waitUntil Function

Slack requires a response within 3 seconds. The waitUntil function allows processing to continue after responding:
waitUntil(handleNewAppMention(event, botUserId));
This prevents duplicate responses caused by Slack retries.

Multi-Step Conversations

The stopWhen: stepCountIs(10) parameter enables the bot to:
  1. Call tools as needed
  2. Process tool results
  3. Generate follow-up responses
  4. Continue until the task is complete

Status Updates

Provide real-time feedback while processing:
updateStatus?.(`is searching the web for ${query}...`);

Deployment

Deploy to Vercel:
pnpm install -g vercel
vercel deploy
Configure environment variables in Vercel dashboard, then update your Slack app’s Event Subscriptions URL:
https://your-vercel-url.vercel.app/api/events
Subscribe to these events:
  • app_mention
  • assistant_thread_started
  • message:im

Testing

In Slack:
  1. Send a DM to the bot
  2. Mention the bot in a channel: @bot What's the weather in London?
  3. Ask it to search: @bot What's the latest news from BBC?

Next Steps

  • Add user-specific memory
  • Implement database queries
  • Add rich message formatting with blocks
  • Create custom slash commands
  • Add analytics and usage tracking

Resources