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
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
- Go to api.slack.com/apps
- Click “Create New App” and choose “From scratch”
- Give your app a name and select your workspace
- Under “OAuth & Permissions”, add bot token scopes:
app_mentions:read
chat:write
im:history
im:write
assistant:write
- Install the app to your workspace
- 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}¤t=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:
- Call tools as needed
- Process tool results
- Generate follow-up responses
- 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:
- Send a DM to the bot
- Mention the bot in a channel:
@bot What's the weather in London?
- 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