> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/vercel/ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Slackbot with AI SDK

> Build an AI-powered Slack bot using the AI SDK with tools and multi-step conversations

# 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](https://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:

```bash theme={null}
git clone https://github.com/vercel-labs/ai-sdk-slackbot.git
cd ai-sdk-slackbot
git checkout starter
pnpm install
```

Create a `.env` file:

```env theme={null}
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:

```ts filename="api/events.ts" theme={null}
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:

```ts filename="lib/generate-response.ts" theme={null}
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:

```ts filename="lib/handle-app-mention.ts" theme={null}
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:

```ts filename="lib/handle-messages.ts" theme={null}
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:

```ts theme={null}
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:

```ts theme={null}
updateStatus?.(`is searching the web for ${query}...`);
```

## Deployment

Deploy to Vercel:

```bash theme={null}
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

* [Full source code](https://github.com/vercel-labs/ai-sdk-slackbot)
* [Slack API documentation](https://api.slack.com/)
* [Tools documentation](/docs/foundations/tools)
* [Multi-step calls](/docs/ai-sdk-core/tools-and-tool-calling#multi-step-calls)
