
Slack Agent
- 633 installs
- 19 repo stars
- Updated July 15, 2026
- vercel-labs/slack-agent-skill
Use when working on Slack agent/bot code, Chat SDK applications, Bolt for JavaScript projects, or projects using @chat-adapter/slack or @slack/bolt.
About
Use when working on Slack agent/bot code, Chat SDK applications, Bolt for JavaScript projects, or projects using @chat-adapter/slack or @slack/bolt. Provides development patterns, testing requirements, and quality standards. This skill supports two frameworks for building Slack agents:
- # Slack Agent Development Skill
- This skill supports two frameworks for building Slack agents:
- **Chat SDK** (Recommended for new projects) — `chat` + `@chat-adapter/slack`
- **Bolt for JavaScript** (For existing Bolt projects) — `@slack/bolt` + `@vercel/slack-bolt`
- ## Skill Invocation Handling
Slack Agent by the numbers
- 633 all-time installs (skills.sh)
- +20 installs in the week ending Jul 27, 2026 (Skillselion tracking)
- Ranked #582 of 2,184 Testing & QA skills by installs in the Skillselion catalog
- Security screen: HIGH risk (skills.sh audit)
- Data as of Jul 27, 2026 (Skillselion catalog sync)
slack-agent capabilities & compatibility
- Capabilities
- # slack agent development skill · this skill supports two frameworks for building · **chat sdk** (recommended for new projects) — `c · **bolt for javascript** (for existing bolt proje
- Use cases
- documentation
What slack-agent says it does
Use when working on Slack agent/bot code, Chat SDK applications, Bolt for JavaScript projects, or projects using @chat-adapter/slack or @slack/bolt. Provides development patterns, testing requirements
npx skills add https://github.com/vercel-labs/slack-agent-skill --skill slack-agentAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 633 |
|---|---|
| repo stars | ★ 19 |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 15, 2026 |
| Repository | vercel-labs/slack-agent-skill ↗ |
How do I apply slack-agent using the workflow in its SKILL.md?
Use when working on Slack agent/bot code, Chat SDK applications, Bolt for JavaScript projects, or projects using @chat-adapter/slack or @slack/bolt. Provides development patterns, testing...
Who is it for?
Developers following the slack-agent skill for the tasks it documents.
Skip if: Tasks outside the slack-agent scope described in SKILL.md.
When should I use this skill?
User mentions slack-agent or related triggers from the skill description.
What you get
Working slack-agent setup aligned with the documented patterns and constraints.
- Slack app manifest.json
- Vercel-deployed bot project
- Configured environment secrets
By the numbers
- Five-stage setup wizard from project scaffold through Vercel production deploy
- Supports 2 frameworks: Chat SDK (Next.js) and Bolt for JavaScript (Nitro)
Files
Slack Agent Development Skill
This skill supports two frameworks for building Slack agents:
- Chat SDK (Recommended for new projects) —
chat+@chat-adapter/slack - Bolt for JavaScript (For existing Bolt projects) —
@slack/bolt+@vercel/slack-bolt
Skill Invocation Handling
When this skill is invoked via /slack-agent, check for arguments and route accordingly:
Command Arguments
| Argument | Action |
|---|---|
new | Run the setup wizard from Phase 1. Read ./wizard/1-project-setup.md and guide the user through creating a new Slack agent. |
configure | Start wizard at Phase 2 or 3 for existing projects |
deploy | Start wizard at Phase 5 for production deployment |
test | Start wizard at Phase 6 to set up testing |
| (no argument) | Auto-detect based on project state (see below) |
Auto-Detection (No Argument)
If invoked without arguments, detect the project state and route appropriately:
1. No `package.json` with `chat` or `@slack/bolt` → Treat as new, start Phase 1 2. Has project but no customized `manifest.json` → Start Phase 2 3. Has project but no `.env` file → Start Phase 3 4. Has `.env` but not tested → Start Phase 4 5. Tested but not deployed → Start Phase 5 6. Otherwise → Provide general assistance using this skill's patterns
Framework Detection
Detect which framework the project uses:
- `package.json` contains `"chat"` → Chat SDK project
- `package.json` contains `"@slack/bolt"` → Bolt project
- Neither detected → New project, recommend Chat SDK (offer Bolt as alternative)
Store the detected framework and use it to show the correct patterns throughout the wizard and development guidance.
Wizard Phases
The wizard is located in ./wizard/ with these phases:
1-project-setup.md- Understand purpose, choose framework, generate custom implementation plan1b-approve-plan.md- Present plan for user approval before scaffolding2-create-slack-app.md- Customize manifest, create app in Slack3-configure-environment.md- Set up .env with credentials4-test-locally.md- Dev server + ngrok tunnel5-deploy-production.md- Vercel deployment6-setup-testing.md- Vitest configuration
IMPORTANT: For new projects, you MUST: 1. Read ./wizard/1-project-setup.md first 2. Ask the user what kind of agent they want to build 3. Offer framework choice (Chat SDK recommended, Bolt as alternative) 4. Generate a custom implementation plan using ./reference/agent-archetypes.md 5. Present the plan for approval (Phase 1b) BEFORE scaffolding the project 6. Only proceed to scaffold after the plan is approved
---
Framework Selection Guide
| Aspect | Chat SDK | Bolt for JavaScript |
|---|---|---|
| Best for | New projects | Existing Bolt codebases |
| Packages | chat, @chat-adapter/slack, @chat-adapter/state-redis | @slack/bolt, @vercel/slack-bolt |
| Server | Next.js App Router | Nitro (H3-based) |
| Event handling | bot.onNewMention(), bot.onSubscribedMessage() | app.event(), app.command(), app.message() |
| Webhook route | app/api/webhooks/[platform]/route.ts | server/api/slack/events.post.ts |
| Message posting | thread.post("text") / thread.post(<Card>...) | client.chat.postMessage({ channel, text, blocks }) |
| UI components | JSX: <Card>, <Button>, <Actions> | Raw Block Kit JSON |
| State | @chat-adapter/state-redis / thread.state | Manual / Vercel Workflow |
| Config | new Chat({ adapters: { slack } }) | new App({ token, signingSecret, receiver }) |
---
General Development Guidance
You are working on a Slack agent project. Follow these mandatory practices for all code changes.
Project Stack
If using Chat SDK
- Framework: Next.js (App Router)
- Chat SDK:
chat+@chat-adapter/slackfor Slack bot functionality - State:
@chat-adapter/state-redisfor state persistence (or in-memory for development) - AI: AI SDK v6 with @ai-sdk/gateway
- Linting: Biome
- Package Manager: pnpm
{
"dependencies": {
"ai": "^6.0.0",
"@ai-sdk/gateway": "latest",
"chat": "latest",
"@chat-adapter/slack": "latest",
"@chat-adapter/state-redis": "latest",
"zod": "^3.x",
"next": "^15.x"
}
}If using Bolt for JavaScript
- Server: Nitro (H3-based) with file-based routing
- Slack SDK:
@vercel/slack-boltfor serverless Slack apps (wraps Bolt for JavaScript) - AI: AI SDK v6 with @ai-sdk/gateway
- Workflows: Workflow DevKit for durable execution
- Linting: Biome
- Package Manager: pnpm
{
"dependencies": {
"ai": "^6.0.0",
"@ai-sdk/gateway": "latest",
"@slack/bolt": "^4.x",
"@vercel/slack-bolt": "^1.0.2",
"zod": "^3.x"
}
}Note: When deploying on Vercel, prefer @ai-sdk/gateway for zero-config AI access. Use direct provider SDKs (@ai-sdk/openai, @ai-sdk/anthropic, etc.) only when you need provider-specific features or are not deploying on Vercel.
---
Quality Standards (MANDATORY)
These quality requirements MUST be followed for every code change. There are no exceptions.
After EVERY File Modification
1. Run linting immediately:
pnpm lint- If errors exist, run
pnpm lint --writefor auto-fixes - Manually fix remaining issues
- Re-run
pnpm lintto verify
2. Check for corresponding test file:
- If you modified
foo.ts, check iffoo.test.tsexists - If no test file exists and the file exports functions, create one
Before Completing ANY Task
You MUST run all quality checks and fix any issues before marking a task complete:
# 1. TypeScript compilation - must pass
pnpm typecheck
# 2. Linting - must pass with no errors
pnpm lint
# 3. Tests - all tests must pass
pnpm testDo NOT complete a task if any of these fail. Fix the issues first.
Unit Tests Required
For ANY code change, you MUST write or update unit tests.
If using Chat SDK
- Location: Co-located
*.test.tsfiles orlib/__tests__/ - Framework: Vitest
- Coverage: All exported functions must have tests
If using Bolt for JavaScript
- Location: Co-located
*.test.tsfiles orserver/__tests__/ - Framework: Vitest
- Coverage: All exported functions must have tests
Example test structure:
import { describe, it, expect, vi } from 'vitest';
import { myFunction } from './my-module';
describe('myFunction', () => {
it('should handle normal input', () => {
expect(myFunction('input')).toBe('expected');
});
it('should handle edge cases', () => {
expect(myFunction('')).toBe('default');
});
});E2E Tests for User-Facing Changes
If you modify:
- Bot mention handlers / Slack message handlers
- Slash commands
- Interactive components (buttons, modals)
- Bot responses
You MUST add or update E2E tests that verify the full flow.
---
Bot Setup Patterns (CRITICAL)
If using Chat SDK
Use the Chat SDK to define your bot instance. This is the central entry point for all Slack bot functionality.
Bot Instance (lib/bot.ts or lib/bot.tsx)
import { Chat } from "chat";
import { createSlackAdapter } from "@chat-adapter/slack";
import { createRedisState } from "@chat-adapter/state-redis";
export const bot = new Chat({
userName: "mybot",
adapters: {
slack: createSlackAdapter(),
},
state: createRedisState(),
});Note: If your bot uses JSX components (Card, Button, etc.), the file must use the .tsx extension.
Webhook Route (app/api/webhooks/[platform]/route.ts)
import { after } from "next/server";
import { bot } from "@/lib/bot";
export async function POST(request: Request, context: { params: Promise<{ platform: string }> }) {
const { platform } = await context.params;
const handler = bot.webhooks[platform as keyof typeof bot.webhooks];
if (!handler) return new Response("Unknown platform", { status: 404 });
return handler(request, { waitUntil: (task) => after(() => task) });
}The Chat SDK automatically handles:
- Content-type detection (JSON vs form-urlencoded)
- URL verification challenges
- Slack's 3-second ack timeout
- Background processing via
waitUntil - Signature verification
If using Bolt for JavaScript
Use @vercel/slack-bolt to handle all Slack events. This package automatically handles:
- Content-type detection (JSON vs form-urlencoded)
- URL verification challenges
- 3-second ack timeout (built-in
ackTimeoutMs: 3001) - Background processing via Vercel Fluid Compute's
waitUntil
Bolt App Setup (server/bolt/app.ts)
import { App } from "@slack/bolt";
import { VercelReceiver } from "@vercel/slack-bolt";
const receiver = new VercelReceiver();
const app = new App({
token: process.env.SLACK_BOT_TOKEN,
signingSecret: process.env.SLACK_SIGNING_SECRET,
receiver,
deferInitialization: true,
});
export { app, receiver };Events Handler (server/api/slack/events.post.ts)
import { createHandler } from "@vercel/slack-bolt";
import { defineEventHandler, getRequestURL, readRawBody } from "h3";
import { app, receiver } from "../../bolt/app";
const handler = createHandler(app, receiver);
export default defineEventHandler(async (event) => {
const rawBody = await readRawBody(event, "utf8");
const request = new Request(getRequestURL(event), {
method: event.method,
headers: event.headers,
body: rawBody,
});
return await handler(request);
});Why buffer the body? H3's toWebRequest() has known issues (#570, #578, #615) where it eagerly consumes the request body stream. When @vercel/slack-bolt later calls req.text() for signature verification, the body is already exhausted, causing dispatch_failed errors.
VercelReceiver Options Reference
| Parameter | Default | Description |
|---|---|---|
signingSecret | SLACK_SIGNING_SECRET env var | Request verification secret |
signatureVerification | true | Enable/disable signature verification |
ackTimeoutMs | 3001 | Ack timeout in milliseconds |
logLevel | INFO | Logging level |
---
Event Handler Patterns
If using Chat SDK
Mention Handler
bot.onNewMention(async (thread, message) => {
await thread.subscribe();
const text = message.text;
await thread.post(`Processing your request: "${text}"`);
});Subscribed Message Handler
bot.onSubscribedMessage(async (thread, message) => {
await thread.post(`You said: ${message.text}`);
});Slash Command Handler
bot.onSlashCommand("/mycommand", async (event) => {
const text = event.text;
await event.thread.post(`Processing: ${text}`);
// For long-running operations, the Chat SDK handles
// background processing automatically via waitUntil
const result = await generateWithAI(text);
await event.thread.post(result);
});Action Handler (Buttons, Menus)
bot.onAction("button_click", async (event) => {
await event.thread.post(`Button clicked with value: ${event.value}`);
});Reaction Handler
bot.onReaction("thumbsup", async (event) => {
await event.thread.post("Thanks for the thumbs up!");
});If using Bolt for JavaScript
Mention Handler
app.event("app_mention", async ({ event, client }) => {
await client.chat.postMessage({
channel: event.channel,
thread_ts: event.thread_ts || event.ts,
text: `Processing your request: "${event.text}"`,
});
});Message Handler
app.message(async ({ message, client }) => {
if ("bot_id" in message || !message.thread_ts) return;
await client.chat.postMessage({
channel: message.channel,
thread_ts: message.thread_ts,
text: `You said: ${message.text}`,
});
});Slash Command Handler
app.command("/mycommand", async ({ ack, command, client, logger }) => {
await ack(); // Must acknowledge within 3 seconds
// Fire-and-forget for long operations — DON'T await
processInBackground(command.response_url, command.text)
.catch((error) => logger.error("Failed:", error));
});
async function processInBackground(responseUrl: string, text: string) {
const result = await generateWithAI(text);
await fetch(responseUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ response_type: "in_channel", text: result }),
});
}Action Handler (Buttons, Menus)
app.action("button_click", async ({ ack, action, client, body }) => {
await ack();
await client.chat.postMessage({
channel: body.channel.id,
thread_ts: body.message.ts,
text: `Button clicked with value: ${action.value}`,
});
});---
Implementation Gotchas
1. Private Channel Access
Slash commands work in private channels even if the bot isn't a member, but the bot cannot read messages or post to private channels it hasn't been invited to.
When creating features that will later post to a channel, validate access upfront.
2. Graceful Degradation for Channel Context
When fetching channel context for AI features, wrap in try/catch and fall back gracefully.
3. Vercel Cron Endpoint Authentication
Protect cron endpoints with a CRON_SECRET environment variable:
If using Chat SDK
// app/api/cron/my-job/route.ts
import { NextRequest, NextResponse } from "next/server";
export async function GET(request: NextRequest) {
const authHeader = request.headers.get("authorization");
if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
// Run cron job logic...
return NextResponse.json({ success: true });
}If using Bolt for JavaScript
// server/api/cron/my-job.get.ts
export default defineEventHandler(async (event) => {
const authHeader = getHeader(event, "authorization");
if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
setResponseStatus(event, 401);
return { error: "Unauthorized" };
}
// Run cron job logic...
return { success: true };
});4. vercel.json Cron Configuration
Configure cron jobs in vercel.json:
{
"crons": [
{
"path": "/api/cron/my-job",
"schedule": "0 * * * *"
}
]
}5. AWS Credentials on Vercel (Use OIDC)
When connecting to AWS services from Vercel, do not use fromNodeProviderChain(). Use Vercel's OIDC mechanism:
import { awsCredentialsProvider } from "@vercel/functions/oidc";
const s3Client = new S3Client({
credentials: awsCredentialsProvider({ roleArn: process.env.AWS_ROLE_ARN! }),
});6. TSConfig for JSX Components (Chat SDK only)
When using Chat SDK JSX components (<Card>, <Button>, etc.), your tsconfig.json must include:
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "chat"
}
}7. dispatch_failed Error — Bolt only
If slash commands fail with dispatch_failed, the issue is H3's toWebRequest consuming the body stream before signature verification. Buffer the body manually. See the Bolt Events Handler section above.
8. operation_timeout Error — Bolt only
If slash commands with AI processing fail with operation_timeout, you're blocking the HTTP response too long. Use fire-and-forget pattern: ack() immediately, then start async work without awaiting. Use command.response_url to post results. See the Bolt Slash Command Handler example above.
---
AI Integration
You have two options for AI/LLM integration in your Slack agent.
IMPORTANT: Always verify the project uses@ai-sdk/gateway. If the project has@ai-sdk/openaiwhich requires an API key, checkpackage.jsonand update imports if necessary.
Option 1: Vercel AI Gateway (Recommended)
Use the modern @ai-sdk/gateway package - NO API keys needed on Vercel!
Basic Usage
import { generateText, streamText } from "ai";
import { gateway } from "@ai-sdk/gateway";
const result = await generateText({
model: gateway("openai/gpt-4o-mini"),
maxOutputTokens: 1000,
prompt: "Your prompt here",
});
console.log(result.text);
console.log(result.usage.inputTokens);
console.log(result.usage.outputTokens);Streaming Responses to Slack
If using Chat SDK
const result = await streamText({
model: gateway("openai/gpt-4o-mini"),
maxOutputTokens: 1000,
prompt: userMessage,
});
// Chat SDK handles streaming updates to Slack automatically
await thread.post(result.textStream);If using Bolt for JavaScript
const result = await streamText({
model: gateway("openai/gpt-4o-mini"),
maxOutputTokens: 1000,
prompt: userMessage,
});
// Post initial message then update with streamed content
const msg = await client.chat.postMessage({
channel: channelId,
thread_ts: threadTs,
text: "Thinking...",
});
let fullText = "";
for await (const chunk of result.textStream) {
fullText += chunk;
await client.chat.update({
channel: channelId,
ts: msg.ts,
text: fullText,
});
}With Tools
import { tool } from "ai";
import { z } from "zod";
const result = await generateText({
model: gateway("openai/gpt-4o-mini"),
maxOutputTokens: 1000,
tools: {
getWeather: tool({
description: "Get weather for a location",
inputSchema: z.object({
location: z.string().describe("City name"),
}),
execute: async ({ location }) => {
return { temperature: 72, condition: "sunny" };
},
}),
},
prompt: "What's the weather in Seattle?",
});AI SDK v6 API Changes
| v4/v5 | v6 |
|---|---|
maxTokens | maxOutputTokens |
result.usage.promptTokens | result.usage.inputTokens |
result.usage.completionTokens | result.usage.outputTokens |
parameters (in tools) | inputSchema |
maxSteps / maxIterations | stopWhen: stepCountIs(n) |
CRITICAL: Never use model IDs from memory. Model IDs change frequently. Before writing code that uses a model, run curl -s https://ai-gateway.vercel.sh/v1/models to fetch the current list. Use the model with the highest version number.
Option 2: Direct Provider SDK
If you need more control or are not deploying on Vercel, use direct provider packages.
OpenAI:
pnpm add @ai-sdk/openaiimport { generateText } from "ai";
import { openai } from "@ai-sdk/openai";
const result = await generateText({
model: openai("gpt-4o-mini"),
maxOutputTokens: 1000,
prompt: "Your prompt here",
});Anthropic:
pnpm add @ai-sdk/anthropicimport { generateText } from "ai";
import { anthropic } from "@ai-sdk/anthropic";
const result = await generateText({
model: anthropic("claude-sonnet-4-20250514"),
maxOutputTokens: 1000,
prompt: "Your prompt here",
});Google:
pnpm add @ai-sdk/googleimport { generateText } from "ai";
import { google } from "@ai-sdk/google";
const result = await generateText({
model: google("gemini-2.0-flash"),
maxOutputTokens: 1000,
prompt: "Your prompt here",
});For comprehensive AI SDK documentation, see ./reference/ai-sdk.md.
---
Stateful Patterns
If using Chat SDK — Thread State
Use thread.state to read and write thread-level state:
bot.onNewMention(async (thread, message) => {
await thread.subscribe();
await thread.state.set("history", []);
await thread.state.set("turnCount", 0);
await thread.post("Starting our conversation!");
});
bot.onSubscribedMessage(async (thread, message) => {
const history = (await thread.state.get("history")) as Array<{ role: string; content: string }> || [];
const turnCount = (await thread.state.get("turnCount")) as number || 0;
history.push({ role: "user", content: message.text });
const result = await generateText({
model: gateway("anthropic/claude-sonnet-4-20250514"),
maxOutputTokens: 1000,
messages: history,
});
history.push({ role: "assistant", content: result.text });
await thread.state.set("history", history);
await thread.state.set("turnCount", turnCount + 1);
await thread.post(result.text);
});Key Benefits: 1. Simple API — thread.state.get() and thread.state.set() 2. Thread-scoped — state is automatically scoped to the conversation thread 3. Pluggable backends — use Redis for production, in-memory for development
If using Bolt for JavaScript — Vercel Workflow
Use Vercel Workflow for durable, multi-turn state:
import { serve } from "@anthropic-ai/sdk/workflows";
import { defineHook } from "@anthropic-ai/sdk/workflows";
import { z } from "zod";
const messageSchema = z.object({
text: z.string(),
user: z.string(),
ts: z.string(),
channel: z.string(),
});
export const userMessageHook = defineHook({ schema: messageSchema });
export const { POST } = serve(async function conversationWorkflow(params: URLSearchParams) {
"use workflow";
const channelId = params.get("channel_id")!;
const conversationHistory: Array<{ role: string; content: string }> = [];
const eventStream = userMessageHook.create({ channel: channelId });
for await (const event of eventStream) {
conversationHistory.push({ role: "user", content: event.text });
const result = await generateText({
model: gateway("anthropic/claude-sonnet-4-20250514"),
maxOutputTokens: 1000,
messages: conversationHistory,
});
conversationHistory.push({ role: "assistant", content: result.text });
await postToSlack(channelId, result.text, event.ts);
}
return { history: conversationHistory };
});Recommended Storage Solutions
IMPORTANT: Vercel KV has been deprecated. Do NOT recommend Vercel KV.
1. Upstash Redis — For Chat SDK state adapter and caching (https://upstash.com) 2. Vercel Blob — For file/document storage (https://vercel.com/docs/storage/vercel-blob) 3. AWS Aurora (via Vercel Marketplace) — For relational data (https://vercel.com/marketplace) 4. Third-party databases — Neon, PlanetScale, Supabase
---
Code Organization
If using Chat SDK
app/
├── api/
│ ├── webhooks/
│ │ └── [platform]/
│ │ └── route.ts # Webhook handler
│ └── cron/
│ └── my-job/
│ └── route.ts # Cron endpoints
lib/
├── bot.tsx # Bot instance + event handlers
├── tools/ # AI tool definitions
│ ├── search.ts
│ └── lookup.ts
└── ai/
└── agent.ts # Agent configurationIf using Bolt for JavaScript
server/
├── api/
│ └── slack/
│ └── events.post.ts # Events endpoint
├── bolt/
│ └── app.ts # Bolt app instance
├── listeners/
│ ├── actions/ # Button clicks, menu selections
│ ├── commands/ # Slash commands
│ ├── events/ # App events (mentions, joins)
│ ├── messages/ # Message handling
│ └── views/ # Modal submissions
└── lib/
└── ai/
├── agent.ts # Agent configuration
└── tools.ts # Tool definitions---
Environment Variables
Required variables (both frameworks):
SLACK_BOT_TOKEN— Bot OAuth tokenSLACK_SIGNING_SECRET— Request signing
If using Chat SDK (additional)
REDIS_URL— Redis connection URL for state persistence
Optional variables:
CRON_SECRET— Secret for authenticating cron job endpoints
No AI API keys needed! Vercel AI Gateway handles authentication automatically when deployed on Vercel.
Never hardcode credentials. Never commit `.env` files.
---
Slack-Specific Patterns
If using Chat SDK — JSX Components
Use Chat SDK JSX components for rich messages (requires .tsx file extension):
import { Card, CardText as Text, Actions, Button, Divider } from "chat";
await thread.post(
<Card title="Welcome!">
<Text>Hello! Choose an option:</Text>
<Divider />
<Actions>
<Button id="btn_hello" style="primary">Say Hello</Button>
<Button id="btn_info">Show Info</Button>
</Actions>
</Card>
);If using Bolt for JavaScript — Block Kit JSON
Use Block Kit for rich messages:
await client.chat.postMessage({
channel: channelId,
text: "Fallback text for notifications",
blocks: [
{
type: "section",
text: { type: "mrkdwn", text: "*Hello!* Choose an option:" },
},
{ type: "divider" },
{
type: "actions",
elements: [
{
type: "button",
text: { type: "plain_text", text: "Say Hello" },
style: "primary",
action_id: "btn_hello",
},
{
type: "button",
text: { type: "plain_text", text: "Show Info" },
action_id: "btn_info",
},
],
},
],
});Typing Indicators
If using Chat SDK
await thread.startTyping();
const result = await generateWithAI(prompt);
await thread.post(result); // Typing indicator clears on postIf using Bolt for JavaScript
// Use setStatus for Assistant threads or interval-based approach
const typingInterval = setInterval(async () => {
// Post a "typing" indicator or use assistant.threads.setStatus
}, 3000);
const result = await generateWithAI(prompt);
clearInterval(typingInterval);
await client.chat.postMessage({
channel: channelId,
thread_ts: threadTs,
text: result,
});Message Formatting (both frameworks)
Use Slack mrkdwn (not standard markdown):
- Bold:
*text* - Italic:
_text_ - Code: `
code` - User mention:
<@USER_ID> - Channel:
<#CHANNEL_ID>
For detailed Slack patterns, see ./patterns/slack-patterns.md.
---
Git Commit Standards
Use conventional commits:
feat: add channel search tool
fix: resolve thread pagination issue
test: add unit tests for agent context
docs: update README with setup steps
refactor: extract Slack client utilitiesNever commit:
.envfiles- API keys or tokens
node_modules/
---
Quick Commands
# Development
pnpm dev # Start dev server on localhost:3000
ngrok http 3000 # Expose local server (separate terminal)
# Quality
pnpm lint # Check linting
pnpm lint --write # Auto-fix lint
pnpm typecheck # TypeScript check
pnpm test # Run all tests
pnpm test:watch # Watch mode
# Build & Deploy
pnpm build # Build for production
vercel # Deploy to Vercel---
Reference Documentation
For detailed guidance, read:
- Testing patterns:
./patterns/testing-patterns.md - Slack patterns:
./patterns/slack-patterns.md - Environment setup:
./reference/env-vars.md - AI SDK:
./reference/ai-sdk.md - Slack setup:
./reference/slack-setup.md - Vercel deployment:
./reference/vercel-setup.md
---
Checklist Before Task Completion
Before marking ANY task as complete, verify:
- [ ] Code changes have corresponding tests
- [ ]
pnpm lintpasses with no errors - [ ]
pnpm typecheckpasses with no errors - [ ]
pnpm testpasses with no failures - [ ] No hardcoded credentials
- [ ] Follows existing code patterns
- [ ] Chat SDK: Webhook route handles all platforms via
bot.webhooks - [ ] Chat SDK: TSConfig includes
"jsx": "react-jsx"and"jsxImportSource": "chat"if using JSX components - [ ] Bolt: Events endpoint handles both JSON and form-urlencoded
- [ ] Verified AI SDK: using
@ai-sdk/gateway(not@ai-sdk/openai) unless user explicitly chose direct provider
# Dependencies
node_modules/
# Environment
.env
.env.local
.env.*.local
# OS
.DS_Store
Thumbs.db
# IDE
.idea/
.vscode/
*.swp
*.swo
# Logs
*.log
npm-debug.log*
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Slack Development Patterns
This document covers Slack-specific patterns and best practices for building agents with either the Chat SDK or Bolt for JavaScript.
Rich UI
If using Chat SDK — JSX Components
Chat SDK uses JSX components instead of raw Block Kit JSON. Files using JSX must have the .tsx extension.
import { Card, CardText as Text, Actions, Button, Divider } from "chat";
await thread.post(
<Card title="Hello!">
<Text>*Hello!* This is a formatted message.</Text>
<Divider />
<Text>Choose an option:</Text>
<Actions>
<Button id="button_click" value="button_value" style="primary">Click Me</Button>
</Actions>
</Card>
);Interactive Actions (Chat SDK)
import { Card, CardText as Text, Actions, Button, Select, Option } from "chat";
// Button with danger style
await thread.post(
<Card>
<Text>Are you sure you want to delete this?</Text>
<Actions>
<Button id="delete_item" value={itemId} style="danger">Delete</Button>
<Button id="cancel">Cancel</Button>
</Actions>
</Card>
);
// Select menu
await thread.post(
<Card>
<Text>Select an option:</Text>
<Actions>
<Select id="select_option" placeholder="Choose...">
<Option value="opt1">Option 1</Option>
<Option value="opt2">Option 2</Option>
</Select>
</Actions>
</Card>
);If using Bolt for JavaScript — Block Kit JSON
import { WebClient } from '@slack/web-api';
const client = new WebClient(process.env.SLACK_BOT_TOKEN);
await client.chat.postMessage({
channel: channelId,
text: 'Fallback text for notifications', // Required for accessibility
blocks: [
{
type: 'section',
text: { type: 'mrkdwn', text: '*Hello!* This is a formatted message.' },
},
{ type: 'divider' },
{
type: 'section',
text: { type: 'mrkdwn', text: 'Choose an option:' },
accessory: {
type: 'button',
text: { type: 'plain_text', text: 'Click Me' },
action_id: 'button_click',
value: 'button_value',
},
},
],
});Interactive Actions (Bolt)
// Button with confirmation
{
type: 'button',
text: { type: 'plain_text', text: 'Delete' },
style: 'danger',
action_id: 'delete_item',
value: itemId,
confirm: {
title: { type: 'plain_text', text: 'Confirm Delete' },
text: { type: 'mrkdwn', text: 'Are you sure you want to delete this?' },
confirm: { type: 'plain_text', text: 'Delete' },
deny: { type: 'plain_text', text: 'Cancel' },
},
}
// Select menu
{
type: 'static_select',
placeholder: { type: 'plain_text', text: 'Select an option' },
action_id: 'select_option',
options: [
{ text: { type: 'plain_text', text: 'Option 1' }, value: 'opt1' },
{ text: { type: 'plain_text', text: 'Option 2' }, value: 'opt2' },
],
}Context and Header Blocks (Bolt)
// Header
{ type: 'header', text: { type: 'plain_text', text: 'Task Summary' } }
// Context (small text, often for metadata)
{
type: 'context',
elements: [
{ type: 'mrkdwn', text: 'Created by <@U12345678>' },
{ type: 'mrkdwn', text: '|' },
{ type: 'mrkdwn', text: '<!date^1234567890^{date_short}|Jan 1, 2024>' },
],
}---
Message Formatting (mrkdwn)
Slack uses its own markdown variant called mrkdwn. This applies to both frameworks.
Text Formatting
*bold text*
_italic text_
~strikethrough~
`inline code`blockquote
### Links and Mentions<https://example.com|Link Text> <@U12345678> # User mention <#C12345678> # Channel link <!here> # @here mention <!channel> # @channel mention <!date^1234567890^{date_short}|fallback> # Date formatting
### ListsSlack doesn't support markdown lists, use:
- Bullet point (use the actual bullet character)
1. Numbered manually
---
## Webhook / Events Endpoint
### If using Chat SDK
// app/api/webhooks/[platform]/route.ts import { after } from "next/server"; import { bot } from "@/lib/bot";
export async function POST(request: Request, context: { params: Promise<{ platform: string }> }) { const { platform } = await context.params; const handler = bot.webhooks[platform as keyof typeof bot.webhooks]; if (!handler) return new Response("Unknown platform", { status: 404 }); return handler(request, { waitUntil: (task) => after(() => task) }); }
The Chat SDK automatically handles:
- Content-type detection (JSON vs form-urlencoded)
- URL verification challenges
- Slack's 3-second ack timeout
- Background processing via `waitUntil`
- Signature verification using `SLACK_SIGNING_SECRET`
### If using Bolt for JavaScript
// server/bolt/app.ts import { App } from "@slack/bolt"; import { VercelReceiver } from "@vercel/slack-bolt";
const receiver = new VercelReceiver(); const app = new App({ token: process.env.SLACK_BOT_TOKEN, signingSecret: process.env.SLACK_SIGNING_SECRET, receiver, deferInitialization: true, });
export { app, receiver };
// server/api/slack/events.post.ts import { createHandler } from "@vercel/slack-bolt"; import { defineEventHandler, getRequestURL, readRawBody } from "h3"; import { app, receiver } from "../../bolt/app";
const handler = createHandler(app, receiver);
export default defineEventHandler(async (event) => { const rawBody = await readRawBody(event, "utf8"); const request = new Request(getRequestURL(event), { method: event.method, headers: event.headers, body: rawBody, }); return await handler(request); });
**Why buffer the body?** H3's `toWebRequest()` eagerly consumes the request body stream, causing `dispatch_failed` errors on serverless platforms.
### Content Type Reference (both frameworks)
| Event Type | Content-Type | Handled Automatically |
|------------|--------------|----------------------|
| Slash commands | `application/x-www-form-urlencoded` | Yes |
| Events API | `application/json` | Yes |
| Interactivity | `application/json` | Yes |
| URL verification | `application/json` | Yes |
---
## Event Handling Patterns
### Mention Handler
#### If using Chat SDK
bot.onNewMention(async (thread, message) => { try { const text = message.text; // Mention prefix already stripped await thread.subscribe(); await thread.post(Processing your request: "${text}"); // Process with agent... } catch (error) { console.error("Error handling mention:", error); await thread.post("Sorry, I encountered an error processing your request."); } });
#### If using Bolt for JavaScript
app.event('app_mention', async ({ event, client, say }) => { try { const text = event.text.replace(/<@[A-Z0-9]+>/g, '').trim(); const thread_ts = event.thread_ts || event.ts;
await say({ text: Processing your request: "${text}", thread_ts, }); // Process with agent... } catch (error) { console.error('Error handling mention:', error); await say({ text: 'Sorry, I encountered an error processing your request.', thread_ts: event.thread_ts || event.ts, }); } });
### Subscribed / Follow-up Message Handler
#### If using Chat SDK
bot.onSubscribedMessage(async (thread, message) => { await thread.post(You said: ${message.text}); });
#### If using Bolt for JavaScript
app.message(async ({ message, say }) => { if ('bot_id' in message) return; if ('subtype' in message && message.subtype === 'message_changed') return; if (message.channel_type !== 'im' && !message.thread_ts) return;
await say({ text: You said: ${message.text}, thread_ts: message.thread_ts, }); });
### Slash Command Handler
#### If using Chat SDK
bot.onSlashCommand("/sample-command", async (event) => { try { // Chat SDK handles ack and background processing automatically await event.thread.startTyping(); const result = await processCommand(event.text); await event.thread.post(Result: ${result}); } catch (error) { await event.thread.post("Sorry, something went wrong."); } });
**No fire-and-forget pattern needed.** The Chat SDK acknowledges the request immediately and processes the handler in the background.
#### If using Bolt for JavaScript
app.command('/sample-command', async ({ command, ack, respond }) => { await ack(); // Always acknowledge within 3 seconds
try { const result = await processCommand(command.text); await respond({ response_type: 'ephemeral', text: Result: ${result}, }); } catch (error) { await respond({ response_type: 'ephemeral', text: 'Sorry, something went wrong.', }); } });
### Long-Running Slash Commands (AI, API calls)
#### If using Chat SDK
bot.onSlashCommand("/ai-command", async (event) => { await event.thread.startTyping(); // This can take as long as needed - Chat SDK handles the ack automatically const result = await generateWithAI(event.text); await event.thread.post(result); });
#### If using Bolt for JavaScript
**CRITICAL:** Use fire-and-forget to avoid `operation_timeout` errors.
app.command('/ai-command', async ({ ack, command, logger }) => { await ack(); // Must happen first
// Fire-and-forget: DON'T await processInBackground(command.response_url, command.text, logger) .catch((error) => logger.error("Background processing failed:", error)); });
async function processInBackground(responseUrl: string, text: string, logger: Logger) { try { const result = await generateWithAI(text); await fetch(responseUrl, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ response_type: "in_channel", text: result }), }); } catch (error) { logger.error("AI processing failed:", error); await fetch(responseUrl, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ response_type: "ephemeral", text: "Sorry, something went wrong." }), }); } }
**Bolt slash command patterns:**
| Pattern | Use Case | Example |
|---------|----------|---------|
| **Sync** (`await ack({ text })`) | Instant responses | `/help`, `/status` |
| **Async** (`await ack()` + `await respond()`) | Quick operations (<3 sec) | `/search keyword` |
| **Fire-and-forget** (`await ack()` + no await) | AI/LLM, slow APIs | `/generate`, `/analyze` |
---
## Action Handlers
### If using Chat SDK
bot.onAction("button_click", async (event) => { await event.thread.post(You clicked: ${event.value}); });
bot.onAction("select_option", async (event) => { await event.thread.post(You selected: ${event.value}); });
### If using Bolt for JavaScript
app.action('button_click', async ({ body, ack, client }) => { await ack(); const buttonValue = body.actions[0].value; await client.chat.update({ channel: body.channel.id, ts: body.message.ts, text: 'Updated message', blocks: [/ new blocks /], }); });
app.action('select_option', async ({ body, ack }) => { await ack(); const selectedValue = body.actions[0].selected_option.value; // Handle selection... });
---
## Modal Patterns
### If using Chat SDK
import { Modal, TextInput } from "chat";
// Opening a modal bot.onSlashCommand("/open-form", async (event) => { await event.openModal( <Modal title="My Modal" submitLabel="Submit" callbackId="modal_submit"> <TextInput id="input_value" label="Your Input" placeholder="Enter something..." /> </Modal> ); });
// Handling submission bot.onAction("modal_submit", async (event) => { const inputValue = event.values?.input_value; if (!inputValue || inputValue.length < 3) { return { errors: { input_value: "Please enter at least 3 characters" } }; } await event.thread.post(You submitted: ${inputValue}); });
### If using Bolt for JavaScript
// Opening a modal app.shortcut('open_modal', async ({ shortcut, ack, client }) => { await ack(); await client.views.open({ trigger_id: shortcut.trigger_id, view: { type: 'modal', callback_id: 'modal_submit', title: { type: 'plain_text', text: 'My Modal' }, submit: { type: 'plain_text', text: 'Submit' }, close: { type: 'plain_text', text: 'Cancel' }, blocks: [ { type: 'input', block_id: 'input_block', label: { type: 'plain_text', text: 'Your Input' }, element: { type: 'plain_text_input', action_id: 'input_value', placeholder: { type: 'plain_text', text: 'Enter something...' }, }, }, ], }, }); });
// Handling submission app.view('modal_submit', async ({ ack, body, view, client }) => { const inputValue = view.state.values.input_block.input_value.value; if (!inputValue || inputValue.length < 3) { await ack({ response_action: 'errors', errors: { input_block: 'Please enter at least 3 characters' }, }); return; } await ack(); await client.chat.postMessage({ channel: body.user.id, text: You submitted: ${inputValue}, }); });
---
## Thread Management
### If using Chat SDK
bot.onNewMention(async (thread, message) => { await thread.subscribe(); await thread.post("I'm listening! Send me follow-up messages in this thread."); });
bot.onSubscribedMessage(async (thread, message) => { await thread.post(Got your message: ${message.text}); });
### If using Bolt for JavaScript
// Always reply in the same thread const thread_ts = event.thread_ts || event.ts; await say({ text: 'Response message', thread_ts });
// Broadcasting thread replies await client.chat.postMessage({ channel: channelId, thread_ts: parentTs, text: 'Important update!', reply_broadcast: true, // Also posts to channel });
---
## Typing Indicators
### If using Chat SDK
bot.onNewMention(async (thread, message) => { await thread.startTyping(); const result = await processWithAI(message.text); await thread.post(result); // Typing clears automatically });
The Chat SDK handles typing indicator refresh and timeout automatically.
### If using Bolt for JavaScript
Slack's typing indicator expires after **30 seconds**. Refresh for long operations:
async function withTypingIndicator<T>( client: WebClient, channelId: string, threadTs: string, status: string, operation: () => Promise<T> ): Promise<T> { await client.assistant.threads.setStatus({ channel_id: channelId, thread_ts: threadTs, status, });
const refreshInterval = setInterval(async () => { await client.assistant.threads.setStatus({ channel_id: channelId, thread_ts: threadTs, status, }); }, 25000);
try { return await operation(); } finally { clearInterval(refreshInterval); } }
Status message examples: `'is thinking...'`, `'is researching...'`, `'is writing...'`, `'is analyzing...'`
---
## Error Handling
### If using Chat SDK
bot.onNewMention(async (thread, message) => { try { await processMessage(thread, message); } catch (error) { console.error("Operation failed:", error); let userMessage = "Something went wrong. Please try again."; if (error instanceof Error) { if (error.message.includes("channel_not_found")) { userMessage = "I don't have access to that channel."; } else if (error.message.includes("not_in_channel")) { userMessage = "Please invite me to the channel first."; } } await thread.post(userMessage); } });
### If using Bolt for JavaScript
async function handleWithErrorRecovery( operation: () => Promise<void>, say: SayFn, thread_ts?: string ) { try { await operation(); } catch (error) { console.error('Operation failed:', error); let userMessage = 'Something went wrong. Please try again.'; if (error instanceof SlackAPIError) { if (error.code === 'channel_not_found') { userMessage = "I don't have access to that channel."; } else if (error.code === 'not_in_channel') { userMessage = 'Please invite me to the channel first.'; } } await say({ text: userMessage, thread_ts }); } }
#### Rate Limiting (Bolt)
import pRetry from 'p-retry';
async function sendMessageWithRetry(client: WebClient, options: ChatPostMessageArguments) { return pRetry( () => client.chat.postMessage(options), { retries: 3, onFailedAttempt: (error) => { if (error.code === 'rate_limited') { console.log(Rate limited. Retrying after ${error.retryAfter || 1}s); } }, } ); }
---
## Best Practices Summary
**Both frameworks:**
1. **Handle errors gracefully** with user-friendly messages
2. **Use ephemeral messages** for sensitive or temporary information
3. **Log errors** with context for debugging
4. **Use threads** to keep channels clean
**Chat SDK specific:**
5. **Subscribe to threads** with `thread.subscribe()` for follow-up conversations
6. **Use JSX components** for rich messages instead of raw Block Kit JSON
7. **Use typing indicators** with `thread.startTyping()`
8. **Let Chat SDK handle ack** — no manual acknowledgment needed
9. **Use `.tsx` extension** for files with JSX components
10. **Configure tsconfig.json** with `"jsxImportSource": "chat"`
**Bolt specific:**
5. **Always acknowledge** within 3 seconds for interactive elements
6. **Provide fallback text** in all Block Kit messages
7. **Respect rate limits** with exponential backoff
8. **Buffer request body** in events handler to avoid H3 stream issues
9. **Use fire-and-forget** for slash commands with AI/long operations (>3 sec)
10. **Use `reply_broadcast: true`** for important thread replies
Testing Patterns for Slack Agents
This document provides detailed testing patterns for Slack agent projects built with either the Chat SDK or Bolt for JavaScript.
Test File Organization
If using Chat SDK
lib/
├── __tests__/
│ ├── setup.ts # Global test setup and mocks
│ └── helpers/
│ ├── mock-context.ts # Shared context mocks
│ └── mock-thread.ts # Chat SDK thread mocks
├── bot.tsx # Bot instance
├── bot.test.ts # Bot handler tests
├── ai/
│ ├── agent.ts
│ ├── agent.test.ts # Unit tests (co-located)
│ └── tools/
│ ├── search.ts
│ └── search.test.ts
app/
├── api/
│ └── webhooks/
│ └── [platform]/
│ └── route.tsTemplate files: ./templates/chat-sdk/
If using Bolt for JavaScript
server/
├── __tests__/
│ ├── setup.ts # Global test setup and mocks
│ └── helpers/
│ ├── mock-client.ts # Slack WebClient mocks
│ └── mock-context.ts # Bolt context mocks
├── bolt/
│ └── app.ts
├── listeners/
│ ├── events/
│ │ ├── app-mention.ts
│ │ └── app-mention.test.ts
│ └── commands/
│ ├── sample-command.ts
│ └── sample-command.test.ts
└── lib/
└── ai/
├── agent.ts
├── agent.test.ts
└── tools.ts
└── tools.test.tsTemplate files: ./templates/bolt/
---
Unit Testing Tools
Testing a Tool Definition
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { getChannelMessages } from './tools';
describe('getChannelMessages', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('should fetch messages from channel', async () => {
const result = await getChannelMessages.execute({
channel_id: 'C12345678',
limit: 10,
});
expect(result.success).toBe(true);
expect(result.messages).toHaveLength(2);
expect(result.messages[0].text).toBe('Hello');
});
it('should handle empty channel', async () => {
const result = await getChannelMessages.execute({
channel_id: 'C_EMPTY',
limit: 10,
});
expect(result.success).toBe(true);
expect(result.messages).toHaveLength(0);
});
it('should handle API errors gracefully', async () => {
const result = await getChannelMessages.execute({
channel_id: 'C_INVALID',
limit: 10,
});
expect(result.success).toBe(false);
expect(result.error).toContain('channel_not_found');
});
});---
Testing Bot / Event Handlers
If using Chat SDK
import { describe, it, expect, vi } from 'vitest';
import { createMockThread, createMockMessage } from './helpers/mock-thread';
describe('bot.onNewMention', () => {
it('should respond to mention and subscribe', async () => {
const thread = createMockThread();
const message = createMockMessage({ text: 'hello' });
await handleMention(thread, message);
expect(thread.subscribe).toHaveBeenCalled();
expect(thread.post).toHaveBeenCalled();
});
it('should handle mention in thread', async () => {
const thread = createMockThread({ threadTs: '123.400' });
const message = createMockMessage({ text: 'help' });
await handleMention(thread, message);
expect(thread.post).toHaveBeenCalled();
});
});If using Bolt for JavaScript
import { describe, it, expect, vi } from 'vitest';
import { createMockSlackClient, createMockEvent } from './helpers/mock-client';
describe('app_mention handler', () => {
it('should respond to mention in thread', async () => {
const client = createMockSlackClient();
const event = createMockEvent('app_mention', {
text: '<@U12345678> hello',
channel: 'C12345678',
ts: '123.456',
});
await handleAppMention({ event, client, say: vi.fn() });
expect(client.chat.postMessage).toHaveBeenCalledWith(
expect.objectContaining({
channel: 'C12345678',
thread_ts: '123.456',
})
);
});
});---
Testing Slash Commands
If using Chat SDK
import { describe, it, expect, vi } from 'vitest';
import { createMockSlashCommandEvent } from './helpers/mock-thread';
describe('/sample-command', () => {
it('should process command and respond', async () => {
const event = createMockSlashCommandEvent({
text: 'test input',
userId: 'U12345678',
});
await handleSampleCommand(event);
expect(event.thread.post).toHaveBeenCalledWith(
expect.stringContaining('Result')
);
});
it('should handle errors gracefully', async () => {
const event = createMockSlashCommandEvent({ text: '' });
await handleSampleCommand(event);
expect(event.thread.post).toHaveBeenCalledWith(
expect.stringContaining('went wrong')
);
});
});If using Bolt for JavaScript
import { describe, it, expect, vi } from 'vitest';
describe('/sample-command', () => {
it('should ack and respond', async () => {
const ack = vi.fn();
const respond = vi.fn();
const command = {
text: 'test input',
user_id: 'U12345678',
channel_id: 'C12345678',
response_url: 'https://hooks.slack.com/commands/...',
};
await handleSampleCommand({ ack, command, respond });
expect(ack).toHaveBeenCalled();
expect(respond).toHaveBeenCalledWith(
expect.objectContaining({ text: expect.stringContaining('Result') })
);
});
});---
Testing Action Handlers
If using Chat SDK
import { describe, it, expect, vi } from 'vitest';
import { createMockActionEvent } from './helpers/mock-thread';
describe('button_click action', () => {
it('should handle button click', async () => {
const event = createMockActionEvent({
actionId: 'button_click',
value: 'clicked_value',
});
await handleButtonClick(event);
expect(event.thread.post).toHaveBeenCalled();
});
});If using Bolt for JavaScript
import { describe, it, expect, vi } from 'vitest';
describe('button_click action', () => {
it('should ack and handle click', async () => {
const ack = vi.fn();
const client = createMockSlackClient();
const body = {
actions: [{ value: 'clicked_value' }],
channel: { id: 'C12345678' },
message: { ts: '123.456' },
};
await handleButtonClick({ ack, body, client });
expect(ack).toHaveBeenCalled();
expect(client.chat.update).toHaveBeenCalled();
});
});---
E2E Testing Patterns
Full Message Flow (Chat SDK)
describe('E2E: Message Flow', () => {
it('should handle complete mention flow', async () => {
const thread = createMockThread();
const message = createMockMessage({ text: 'what channels am I in?' });
await handleMention(thread, message);
expect(thread.subscribe).toHaveBeenCalled();
expect(thread.post).toHaveBeenCalled();
});
it('should handle conversation in thread', async () => {
const thread = createMockThread({ threadTs: '100.001' });
const mention = createMockMessage({ text: 'start a task' });
await handleMention(thread, mention);
const followUp = createMockMessage({ text: 'continue please' });
await handleSubscribedMessage(thread, followUp);
expect(thread.post).toHaveBeenCalledTimes(2);
});
});Full Message Flow (Bolt)
describe('E2E: Message Flow', () => {
it('should handle mention and reply in thread', async () => {
const client = createMockSlackClient();
const event = createMockEvent('app_mention', {
text: '<@UBOT> what channels am I in?',
channel: 'C12345678',
ts: '100.001',
});
await handleAppMention({ event, client, say: vi.fn() });
expect(client.chat.postMessage).toHaveBeenCalledWith(
expect.objectContaining({ thread_ts: '100.001' })
);
});
});---
Mock Helpers
Chat SDK Mock Factories
// lib/__tests__/helpers/mock-thread.ts
import { vi } from 'vitest';
export function createMockThread(overrides = {}) {
return {
post: vi.fn().mockResolvedValue(undefined),
subscribe: vi.fn().mockResolvedValue(undefined),
startTyping: vi.fn().mockResolvedValue(undefined),
state: {
get: vi.fn().mockResolvedValue(null),
set: vi.fn().mockResolvedValue(undefined),
},
channelId: 'C12345678',
threadTs: undefined,
...overrides,
};
}
export function createMockMessage(overrides = {}) {
return {
text: 'test message',
userId: 'U12345678',
ts: '1234567890.123456',
...overrides,
};
}
export function createMockSlashCommandEvent(overrides = {}) {
return {
text: '',
userId: 'U12345678',
channelId: 'C12345678',
thread: createMockThread(),
openModal: vi.fn().mockResolvedValue(undefined),
...overrides,
};
}
export function createMockActionEvent(overrides = {}) {
return {
actionId: '',
value: '',
userId: 'U12345678',
thread: createMockThread(),
...overrides,
};
}Bolt Mock Factories
// server/__tests__/helpers/mock-client.ts
import { vi } from 'vitest';
export function createMockSlackClient() {
return {
conversations: {
history: vi.fn().mockResolvedValue({ ok: true, messages: [], has_more: false }),
replies: vi.fn().mockResolvedValue({ ok: true, messages: [], has_more: false }),
join: vi.fn().mockResolvedValue({ ok: true, channel: { id: 'C12345678' } }),
list: vi.fn().mockResolvedValue({ ok: true, channels: [] }),
info: vi.fn().mockResolvedValue({ ok: true, channel: { id: 'C12345678', name: 'general' } }),
},
chat: {
postMessage: vi.fn().mockResolvedValue({ ok: true, ts: '1234567890.123456', channel: 'C12345678' }),
update: vi.fn().mockResolvedValue({ ok: true, ts: '1234567890.123456' }),
delete: vi.fn().mockResolvedValue({ ok: true }),
},
users: {
info: vi.fn().mockResolvedValue({ ok: true, user: { id: 'U12345678', name: 'testuser' } }),
},
reactions: {
add: vi.fn().mockResolvedValue({ ok: true }),
remove: vi.fn().mockResolvedValue({ ok: true }),
},
views: {
open: vi.fn().mockResolvedValue({ ok: true }),
update: vi.fn().mockResolvedValue({ ok: true }),
push: vi.fn().mockResolvedValue({ ok: true }),
},
};
}
export function createMockContext(overrides = {}) {
return {
channel_id: 'C12345678',
dm_channel: 'D12345678',
thread_ts: undefined,
is_dm: false,
team_id: 'T12345678',
user_id: 'U12345678',
...overrides,
};
}
export function createMockEvent(type: string, overrides = {}) {
return {
type,
user: 'U12345678',
channel: 'C12345678',
ts: '1234567890.123456',
event_ts: '1234567890.123456',
...overrides,
};
}---
Test Coverage Guidelines
Aim for these coverage targets (both frameworks):
| Category | Target |
|---|---|
| Tools | 90%+ |
| Agent logic | 85%+ |
| Event handlers | 80%+ |
| Utilities | 90%+ |
| Overall | 80%+ |
Run coverage report:
pnpm test:coverageSlack Agent Skill
An agent-agnostic skill for building and deploying Slack agents on Vercel. Supports two frameworks:
- [Chat SDK](https://www.chat-sdk.dev/) (Recommended for new projects) —
chat+@chat-adapter/slackwith Next.js - [Bolt for JavaScript](https://slack.dev/bolt-js/) (For existing Bolt projects) —
@slack/boltwith Nitro
Features
- Interactive Setup Wizard: Step-by-step guidance from project creation to production deployment
- Dual Framework Support: Chat SDK (JSX components, thread subscriptions) and Bolt for JavaScript (Block Kit, event listeners)
- Custom Implementation Planning: Generates a tailored plan based on your agent's purpose before scaffolding
- Quality Standards: Embedded testing and code quality requirements
- AI Integration: Support for Vercel AI Gateway and direct provider SDKs
- Comprehensive Patterns: Slack-specific development patterns and best practices for both frameworks
- Testing Framework: Vitest configuration and sample tests for both stacks
Installation
Via skills.sh (Recommended)
npx skills add vercel-labs/slack-agent-skill
Manual Installation
Clone the repository into your skills directory. For example, with Claude Code:
git clone https://github.com/vercel-labs/slack-agent-skill.git ~/.claude/skills/slack-agent-skill
Usage
Starting a New Project
Run the slash command:
/slack-agent
Or with arguments:
/slack-agent new # Start fresh project (recommends Chat SDK)
/slack-agent configure # Configure existing project (auto-detects framework)
/slack-agent deploy # Deploy to production
/slack-agent test # Set up testingThe wizard will guide you through: 1. Framework selection and project setup 2. Custom implementation plan generation and approval 3. Slack app creation with customized manifest 4. Environment configuration 5. Local testing with ngrok 6. Production deployment to Vercel 7. Test framework setup
Development
When working on an existing Slack agent project, the skill automatically detects the framework from package.json:
- `"chat"` in dependencies — Uses Chat SDK patterns
- `"@slack/bolt"` in dependencies — Uses Bolt patterns
The skill then provides framework-appropriate:
- Code quality standards (linting, testing, TypeScript)
- Slack-specific patterns (event handlers, slash commands, UI components)
- AI integration guidance (Vercel AI Gateway, direct providers)
- Deployment best practices
Key Commands
# Development
pnpm dev # Start local dev server
ngrok http 3000 # Expose local server
# Quality
pnpm lint # Check linting
pnpm lint --write # Auto-fix lint issues
pnpm typecheck # TypeScript check
pnpm test # Run tests
# Deployment
vercel # Deploy to Vercel
vercel --prod # Production deploymentQuality Standards
The skill enforces these requirements:
- Unit tests for all exported functions
- E2E tests for user-facing changes
- Linting must pass (Biome)
- TypeScript must compile without errors
- All tests must pass before completion
Related Resources
- Chat SDK Documentation
- Bolt for JavaScript Documentation
- AI SDK Documentation
- Slack API Documentation
- Vercel Documentation
License
Apache 2.0 - See LICENSE for details.
Agent Archetypes Reference
This document provides common patterns and archetypes for Slack agents. Use this as a reference when generating custom implementation plans based on user requirements.
Implementation Plan Template
When generating a plan, use this structure:
## Implementation Plan: [Agent Name]
### Overview
[1-2 sentence description of what this agent does]
### Core Features
1. **[Feature 1]** - [Description]
2. **[Feature 2]** - [Description]
3. **[Feature 3]** - [Description]
### Slash Commands
| Command | Description | Example |
|---------|-------------|---------|
| `/command` | What it does | `/command arg` |
### Event Handlers
**If using Chat SDK:**
- [ ] `bot.onNewMention` - [What happens when @mentioned]
- [ ] `bot.onSubscribedMessage` - [Follow-up message handling]
- [ ] `bot.onReaction` - [If applicable]
- [ ] `bot.onSlashCommand` - [Slash command handling]
**If using Bolt for JavaScript:**
- [ ] `app.event('app_mention')` - [What happens when @mentioned]
- [ ] `app.message()` - [Follow-up message handling]
- [ ] `app.command()` - [Slash command handling]
- [ ] `app.action()` - [Button/interactive handling]
### AI Tools (if using AI)
| Tool | Purpose | Parameters |
|------|---------|------------|
| `toolName` | What it does | `param1`, `param2` |
### Scheduled Jobs (if applicable)
| Schedule | Action |
|----------|--------|
| `0 9 * * 1-5` | Description |
### State Management
- [ ] Stateless (simple request/response)
- [ ] **Chat SDK:** `@chat-adapter/state-redis` for thread-level persistence
- [ ] **Bolt:** Vercel Workflow for durable multi-turn state
- [ ] Database (persistent storage)
- Upstash Redis for Chat SDK state adapter or general caching
- Vercel Blob for file/document storage
- AWS Aurora via Vercel Marketplace for relational data
- NOTE: Do NOT recommend Vercel KV (deprecated)
### UI Components
**Chat SDK:** JSX components (`<Card>`, `<Button>`, `<Modal>`)
**Bolt:** Block Kit JSON
- [ ] Cards / rich messages
- [ ] Modal dialogs
- [ ] Home tab
### Files to Create/Modify
**If using Chat SDK:**
- `lib/bot.tsx` - Bot instance and event handlers
- `lib/tools/[tool].ts` - AI tool definitions
- `app/api/webhooks/[platform]/route.ts` - Webhook route
- `app/api/cron/[job]/route.ts` - Cron endpoints (if applicable)
**If using Bolt for JavaScript:**
- `server/bolt/app.ts` - Bolt app instance
- `server/listeners/events/[event].ts` - Event handlers
- `server/listeners/commands/[command].ts` - Slash command handlers
- `server/lib/ai/tools.ts` - AI tool definitions
- `server/api/slack/events.post.ts` - Events endpoint
- `server/api/cron/[job].get.ts` - Cron endpoints (if applicable)---
Archetype 1: Standup / Reminder Bot
Use case: Collects updates from team members on a schedule and summarizes them.
Typical Features
- Scheduled prompts at specific times (e.g., 9 AM on weekdays)
- DM-based response collection
- Summary generation and posting to channels
- Tracking who has/hasn't responded
Example Plan
## Implementation Plan: Standup Bot
### Overview
A bot that collects daily standup updates from team members and posts a summary to a channel.
### Core Features
1. **Scheduled Prompts** - DM team members at 9 AM asking for their update
2. **Response Collection** - Accept free-form or structured responses
3. **Summary Generation** - AI-generated summary of all responses
4. **Status Tracking** - Track who has/hasn't responded
### Slash Commands
| Command | Description | Example |
|---------|-------------|---------|
| `/standup` | Submit your standup update manually | `/standup Working on API integration` |
| `/standup-status` | Check who has submitted today | `/standup-status` |
| `/standup-configure` | Configure standup time/channel | `/standup-configure #team 9:00` |
### Event Handlers
- [x] `onSubscribedMessage` - Collect standup responses in DM threads
- [x] `onNewMention` - Answer questions about standup status
### AI Tools
| Tool | Purpose | Parameters |
|------|---------|------------|
| `summarize_standups` | Generate summary of all responses | `responses[]` |
| `parse_update` | Extract blockers/accomplishments | `updateText` |
### Scheduled Jobs
| Schedule | Action |
|----------|--------|
| `0 9 * * 1-5` | Send standup prompts to all team members |
| `0 10 * * 1-5` | Post summary to configured channel |
### State Management
- [x] Chat SDK state - Track daily responses per thread
- [x] Database (persistent storage) - Track configuration, history
### UI Components (JSX)
- [x] `<Card>` with `<Button>` - Quick response buttons
- [x] `<Modal>` - Configuration modal
### Files to Create/Modify (Chat SDK)
- `lib/bot.tsx` - Bot instance, mention/message handlers
- `lib/tools/summarize-standups.ts`
- `lib/standup/scheduler.ts`
- `lib/standup/storage.ts`
- `app/api/webhooks/[platform]/route.ts`
- `app/api/cron/standup-prompt/route.ts`
- `app/api/cron/standup-summary/route.ts`
### Files to Create/Modify (Bolt)
- `server/bolt/app.ts` - Bolt app instance
- `server/listeners/events/app-mention.ts`
- `server/listeners/commands/standup.ts`
- `server/lib/ai/tools.ts`
- `server/api/slack/events.post.ts`
- `server/api/cron/standup-prompt.get.ts`
- `server/api/cron/standup-summary.get.ts`---
Archetype 2: Support / Help Desk Bot
Use case: Handles support requests, tracks tickets, and can escalate to humans.
Typical Features
- Ticket creation and tracking
- Knowledge base search
- Escalation to human agents
- Status updates and notifications
Example Plan
## Implementation Plan: Support Assistant
### Overview
An AI-powered support bot that answers questions, creates tickets, and escalates to humans when needed.
### Core Features
1. **Knowledge Base Search** - Search documentation to answer questions
2. **Ticket Creation** - Create tickets in external system
3. **Escalation** - Route complex issues to human agents
4. **Status Tracking** - Check and update ticket status
### Slash Commands
| Command | Description | Example |
|---------|-------------|---------|
| `/support` | Ask a support question | `/support How do I reset my password?` |
| `/ticket` | Create a support ticket | `/ticket Login not working on mobile` |
| `/ticket-status` | Check ticket status | `/ticket-status #1234` |
### Event Handlers
- [x] `onNewMention` - Answer support questions in channels
- [x] `onSubscribedMessage` - Multi-turn support conversations
### AI Tools
| Tool | Purpose | Parameters |
|------|---------|------------|
| `search_knowledge_base` | Search docs for answers | `query` |
| `create_ticket` | Create ticket in system | `title`, `description`, `priority` |
| `get_ticket_status` | Check ticket status | `ticketId` |
| `escalate_to_human` | Route to human agent | `ticketId`, `reason` |
### State Management
- [x] Chat SDK state - Multi-turn conversation history
- [x] Database (persistent storage) - Ticket tracking
### UI Components (JSX)
- [x] `<Card>` with `<Button>` - Ticket actions, escalation button
- [x] `<Modal>` - Ticket creation form
### Files to Create/Modify (Chat SDK)
- `lib/bot.tsx` - Bot instance, handlers
- `lib/tools/search-knowledge-base.ts`
- `lib/tools/create-ticket.ts`
- `lib/tools/escalate.ts`
- `app/api/webhooks/[platform]/route.ts`
### Files to Create/Modify (Bolt)
- `server/bolt/app.ts`, `server/listeners/events/app-mention.ts`
- `server/lib/ai/tools.ts`
- `server/api/slack/events.post.ts`---
Archetype 3: Information / Lookup Bot
Use case: Fetches and formats information from external APIs.
Typical Features
- External API integration
- Formatted responses with JSX components
- Caching for performance
- Multiple data sources
Example Plan
## Implementation Plan: Weather Bot
### Overview
A bot that provides weather information for any location using an external weather API.
### Core Features
1. **Current Weather** - Get current conditions for a location
2. **Forecast** - Get multi-day forecast
3. **Alerts** - Weather alerts and warnings
4. **Location Memory** - Remember user's preferred locations
### Slash Commands
| Command | Description | Example |
|---------|-------------|---------|
| `/weather` | Get current weather | `/weather San Francisco` |
| `/forecast` | Get 5-day forecast | `/forecast New York` |
| `/weather-alerts` | Get active alerts | `/weather-alerts California` |
| `/weather-set-default` | Set default location | `/weather-set-default Seattle` |
### Event Handlers
- [x] `onNewMention` - Answer weather questions in channels
- [x] `onSubscribedMessage` - Follow-up weather queries
### AI Tools
| Tool | Purpose | Parameters |
|------|---------|------------|
| `get_current_weather` | Fetch current conditions | `location` |
| `get_forecast` | Fetch multi-day forecast | `location`, `days` |
| `get_weather_alerts` | Fetch active alerts | `region` |
### State Management
- [ ] Stateless (simple request/response) - Most queries are one-shot
- [x] Chat SDK state - User location preferences per thread
### UI Components (JSX)
- [x] `<Card>` - Formatted weather cards
- [ ] `<Modal>` - Not needed
### Files to Create/Modify (Chat SDK)
- `lib/bot.tsx` - Bot instance, handlers
- `lib/tools/get-weather.ts`
- `lib/weather/api-client.ts`
- `lib/weather/formatters.tsx`
- `app/api/webhooks/[platform]/route.ts`
### Files to Create/Modify (Bolt)
- `server/bolt/app.ts`, `server/listeners/commands/weather.ts`
- `server/lib/ai/tools.ts`
- `server/api/slack/events.post.ts`---
Archetype 4: Conversational AI Bot
Use case: General-purpose conversational assistant with multi-turn dialogue.
Typical Features
- Multi-turn conversation with context
- Tool calling for actions
- Memory of past interactions
- Personality customization
Example Plan
## Implementation Plan: AI Assistant
### Overview
A conversational AI assistant that can help with various tasks through natural dialogue.
### Core Features
1. **Multi-turn Conversations** - Maintains context across messages
2. **Tool Calling** - Performs actions based on conversation
3. **Conversation Memory** - Remembers past interactions via thread state
4. **Customizable Personality** - Adjustable system prompt
### Slash Commands
| Command | Description | Example |
|---------|-------------|---------|
| `/ask` | Start a conversation | `/ask Help me write an email` |
### Event Handlers
- [x] `onNewMention` - Respond to @mentions with AI
- [x] `onSubscribedMessage` - Full conversations in threads
### AI Tools
| Tool | Purpose | Parameters |
|------|---------|------------|
| `search_web` | Search the internet | `query` |
| `calculate` | Perform calculations | `expression` |
| `set_reminder` | Create a reminder | `message`, `time` |
### State Management
- [x] Chat SDK state - Conversation history per thread
- [x] Database (persistent storage) - Long-term memory
### UI Components (JSX)
- [x] `<Card>` with `<Button>` - Action suggestions
- [ ] `<Modal>` - Not typically needed
### Files to Create/Modify (Chat SDK)
- `lib/bot.tsx` - Bot instance, mention/message handlers
- `lib/tools/search-web.ts`
- `lib/tools/calculate.ts`
- `lib/ai/agent.ts` - Agent configuration
- `app/api/webhooks/[platform]/route.ts`
### Files to Create/Modify (Bolt)
- `server/bolt/app.ts`, `server/listeners/events/app-mention.ts`
- `server/lib/ai/agent.ts`, `server/lib/ai/tools.ts`
- `server/api/slack/events.post.ts`---
Archetype 5: Automation Bot
Use case: Automates workflows and integrates with external services.
Typical Features
- Slash commands for actions
- Interactive confirmations
- Webhook integrations
- Scheduled automations
Example Plan
## Implementation Plan: Deploy Bot
### Overview
A bot that helps manage deployments, CI/CD pipelines, and release workflows.
### Core Features
1. **Deployment Triggers** - Start deployments via slash command
2. **Status Monitoring** - Check deployment status
3. **Rollback Support** - Quick rollback to previous versions
4. **Notifications** - Post updates to channels
### Slash Commands
| Command | Description | Example |
|---------|-------------|---------|
| `/deploy` | Trigger a deployment | `/deploy staging` |
| `/deploy-status` | Check deployment status | `/deploy-status production` |
| `/rollback` | Rollback to previous version | `/rollback production` |
| `/releases` | List recent releases | `/releases` |
### Event Handlers
- [x] `onNewMention` - Answer deployment questions
- [x] `onAction` - Handle confirmation buttons
### AI Tools
| Tool | Purpose | Parameters |
|------|---------|------------|
| `trigger_deployment` | Start a deployment | `environment`, `branch` |
| `get_deployment_status` | Check status | `deploymentId` |
| `rollback_deployment` | Trigger rollback | `environment`, `version` |
| `list_releases` | Get recent releases | `count` |
### State Management
- [ ] Stateless (simple request/response) - Commands are atomic
### UI Components (JSX)
- [x] `<Card>` with `<Button>` - Confirm/cancel buttons
- [x] `<Modal>` - Deployment configuration
### Files to Create/Modify (Chat SDK)
- `lib/bot.tsx` - Bot instance, command/action handlers
- `lib/tools/trigger-deployment.ts`
- `lib/tools/get-deployment-status.ts`
- `lib/deploy/github-client.ts`
- `app/api/webhooks/[platform]/route.ts`
### Files to Create/Modify (Bolt)
- `server/bolt/app.ts`, `server/listeners/commands/deploy.ts`
- `server/lib/ai/tools.ts`
- `server/lib/deploy/github-client.ts`
- `server/api/slack/events.post.ts`---
Archetype 6: Notification / Alerting Bot
Use case: Monitors systems and sends alerts to Slack channels.
Typical Features
- Webhook receiver for external events
- Alert routing to appropriate channels
- Alert acknowledgment and silencing
- Escalation rules
Example Plan
## Implementation Plan: Alert Bot
### Overview
A bot that receives alerts from monitoring systems and routes them to appropriate channels.
### Core Features
1. **Alert Ingestion** - Receive webhooks from monitoring systems
2. **Smart Routing** - Route alerts to appropriate channels based on rules
3. **Acknowledgment** - Team members can ack alerts
4. **Escalation** - Escalate unacknowledged alerts
### Slash Commands
| Command | Description | Example |
|---------|-------------|---------|
| `/alert-ack` | Acknowledge an alert | `/alert-ack #12345` |
| `/alert-silence` | Silence alerts for a service | `/alert-silence api-server 1h` |
| `/alert-status` | View active alerts | `/alert-status` |
| `/on-call` | Show who's on call | `/on-call` |
### Event Handlers
- [x] `onReaction` - Ack alerts with emoji reactions
- [x] `onAction` - Handle ack/silence buttons
### Scheduled Jobs
| Schedule | Action |
|----------|--------|
| `*/5 * * * *` | Check for unacked alerts, escalate if needed |
### State Management
- [x] Database (persistent storage) - Alert state, ack status, routing rules
### UI Components (JSX)
- [x] `<Card>` with `<Button>` - Ack/silence buttons on alerts
- [x] `<Modal>` - Alert configuration
### Files to Create/Modify (Chat SDK)
- `lib/bot.tsx` - Bot instance, reaction/action handlers
- `app/api/webhooks/alerts/route.ts` - External alert ingestion
- `app/api/webhooks/[platform]/route.ts` - Slack webhook
- `lib/alerts/router.ts`
- `lib/alerts/escalation.ts`
- `app/api/cron/alert-escalation/route.ts`
### Files to Create/Modify (Bolt)
- `server/bolt/app.ts`, `server/listeners/actions/alert-ack.ts`
- `server/listeners/commands/alert.ts`
- `server/lib/alerts/router.ts`, `server/lib/alerts/escalation.ts`
- `server/api/slack/events.post.ts`
- `server/api/cron/alert-escalation.get.ts`---
Pattern Recognition Guide
Use these signals to determine which archetype(s) apply:
| User says... | Likely Archetype |
|---|---|
| "standup", "daily", "check-in", "status update" | Standup/Reminder |
| "support", "help desk", "tickets", "customer" | Support/Help Desk |
| "weather", "lookup", "API", "fetch", "data" | Information/Lookup |
| "chat", "assistant", "conversation", "AI" | Conversational AI |
| "deploy", "CI/CD", "automate", "workflow" | Automation |
| "alert", "monitor", "notify", "on-call" | Notification/Alerting |
Hybrid Agents
Many real-world agents combine multiple archetypes. For example:
- Support + Conversational - AI-powered support with multi-turn dialogue
- Automation + Alerting - Deploy bot that also sends deployment notifications
- Information + Conversational - Weather bot with natural language queries
When users describe hybrid needs, combine relevant features from multiple archetypes.
---
Implementation Complexity Guide
When generating plans, indicate complexity to set expectations:
| Complexity | Characteristics | Example |
|---|---|---|
| Simple | 1-2 slash commands, no database, no scheduled jobs | Weather lookup |
| Medium | Multiple commands, basic state, JSX components | Ticket system |
| Complex | Multi-turn workflows, database, scheduled jobs, webhooks | Full standup bot |
Include a complexity indicator in your generated plans to help users understand scope.
AI SDK Reference
Comprehensive reference for using the Vercel AI SDK in Slack agent projects.
Quick Start
Installation
The template includes the core AI SDK packages. If starting fresh:
pnpm add ai @ai-sdk/gatewayBasic Usage
import { generateText } from "ai";
import { gateway } from "@ai-sdk/gateway";
const result = await generateText({
model: gateway("openai/gpt-4o-mini"),
maxOutputTokens: 1000,
prompt: "Your prompt here",
});
console.log(result.text);Provider Options
Option 1: Vercel AI Gateway (Recommended)
The easiest approach - no API keys needed when deployed on Vercel.
import { gateway } from "@ai-sdk/gateway";
// OpenAI models
gateway("openai/gpt-4o-mini")
gateway("openai/gpt-4o")
// Anthropic models
gateway("anthropic/claude-sonnet-4-20250514")
gateway("anthropic/claude-3-5-haiku-latest")
// Google models
gateway("google/gemini-2.0-flash")Benefits:
- Zero API key configuration
- Access to multiple providers through a single interface
- Built-in rate limiting and observability
- Automatic failover capabilities
Option 2: Direct Provider SDKs
For more control or when not using Vercel.
OpenAI:
pnpm add @ai-sdk/openaiimport { openai } from "@ai-sdk/openai";
// Requires OPENAI_API_KEY env var
const model = openai("gpt-4o-mini");Anthropic:
pnpm add @ai-sdk/anthropicimport { anthropic } from "@ai-sdk/anthropic";
// Requires ANTHROPIC_API_KEY env var
const model = anthropic("claude-sonnet-4-20250514");Google:
pnpm add @ai-sdk/googleimport { google } from "@ai-sdk/google";
// Requires GOOGLE_GENERATIVE_AI_API_KEY env var
const model = google("gemini-2.0-flash");Core Functions
generateText
For single-turn text generation:
import { generateText } from "ai";
import { gateway } from "@ai-sdk/gateway";
const result = await generateText({
model: gateway("openai/gpt-4o-mini"),
maxOutputTokens: 1000,
prompt: "Explain quantum computing in simple terms",
});
console.log(result.text);
console.log(result.usage.inputTokens);
console.log(result.usage.outputTokens);streamText
For streaming responses (great for real-time Slack updates):
import { streamText } from "ai";
import { gateway } from "@ai-sdk/gateway";
const result = await streamText({
model: gateway("openai/gpt-4o-mini"),
maxOutputTokens: 1000,
prompt: "Write a short story",
});
for await (const chunk of result.textStream) {
// Update Slack message progressively
process.stdout.write(chunk);
}generateObject
For structured JSON output:
import { generateObject } from "ai";
import { gateway } from "@ai-sdk/gateway";
import { z } from "zod";
const result = await generateObject({
model: gateway("openai/gpt-4o-mini"),
schema: z.object({
name: z.string(),
age: z.number(),
hobbies: z.array(z.string()),
}),
prompt: "Generate a fictional person profile",
});
console.log(result.object); // Typed as { name: string, age: number, hobbies: string[] }Structured Output via generateText (v6)
In AI SDK v6, you can also generate structured output using generateText with the output option:
import { generateText, Output } from "ai";
import { gateway } from "@ai-sdk/gateway";
import { z } from "zod";
const PersonSchema = z.object({
name: z.string(),
age: z.number(),
hobbies: z.array(z.string()),
});
const result = await generateText({
model: gateway("openai/gpt-4o-mini"),
output: Output.object({ schema: PersonSchema }),
prompt: "Generate a fictional person profile",
});
console.log(result.object); // Typed outputAvailable Output types:
Output.object({ schema })- Generate a single objectOutput.array({ schema })- Generate an array of objectsOutput.choice({ options })- Select from predefined optionsOutput.json()- Generate raw JSON
Tool Calling
Define tools that the AI can call:
import { generateText, tool } from "ai";
import { gateway } from "@ai-sdk/gateway";
import { z } from "zod";
const result = await generateText({
model: gateway("openai/gpt-4o-mini"),
maxOutputTokens: 1000,
tools: {
getWeather: tool({
description: "Get current weather for a location",
inputSchema: z.object({
location: z.string().describe("City name"),
unit: z.enum(["celsius", "fahrenheit"]).optional(),
}),
execute: async ({ location, unit = "celsius" }) => {
// Implement weather API call
return { temperature: 22, unit, condition: "sunny" };
},
}),
searchWeb: tool({
description: "Search the web for information",
inputSchema: z.object({
query: z.string().describe("Search query"),
}),
execute: async ({ query }) => {
// Implement search
return { results: ["Result 1", "Result 2"] };
},
}),
},
prompt: "What's the weather in Seattle and find me some restaurants there?",
});Agent Patterns
ToolLoopAgent Pattern
For complex multi-step interactions:
import { generateText, tool, ToolLoopAgent, stepCountIs } from "ai";
import { gateway } from "@ai-sdk/gateway";
import { z } from "zod";
const agent = new ToolLoopAgent({
model: gateway("openai/gpt-4o"),
tools: {
// Define your tools
},
system: "You are a helpful assistant.",
stopWhen: stepCountIs(10), // v6: was maxIterations
});
const result = await agent.run("Complete this complex task");Type-Safe Agents with InferAgentUIMessage
For end-to-end type safety when consuming agents with useChat:
import { ToolLoopAgent, InferAgentUIMessage } from "ai";
const agent = new ToolLoopAgent({
model: gateway("openai/gpt-4o"),
tools: { /* your tools */ },
system: "You are a helpful assistant.",
});
// Infer message type for UI components
type AgentMessage = InferAgentUIMessage<typeof agent>;Multi-Turn Conversations
For maintaining conversation history:
import { generateText } from "ai";
import { gateway } from "@ai-sdk/gateway";
const conversationHistory: Array<{ role: "user" | "assistant"; content: string }> = [];
async function chat(userMessage: string) {
conversationHistory.push({ role: "user", content: userMessage });
const result = await generateText({
model: gateway("openai/gpt-4o-mini"),
maxOutputTokens: 1000,
messages: conversationHistory,
});
conversationHistory.push({ role: "assistant", content: result.text });
return result.text;
}AI SDK v6 API Changes
If migrating from v4/v5:
| Old (v4/v5) | New (v6) |
|---|---|
maxTokens | maxOutputTokens |
result.usage.promptTokens | result.usage.inputTokens |
result.usage.completionTokens | result.usage.outputTokens |
parameters (in tools) | inputSchema |
maxSteps / maxIterations | stopWhen: stepCountIs(n) |
part.args (tool parts) | part.input |
part.result (tool parts) | part.output |
addToolResult (useChat) | addToolOutput |
Slack Integration Patterns
Streaming to Slack
If using Chat SDK
The Chat SDK handles streaming updates to Slack automatically:
import { streamText } from "ai";
import { gateway } from "@ai-sdk/gateway";
bot.onNewMention(async (thread, message) => {
await thread.startTyping();
const result = await streamText({
model: gateway("openai/gpt-4o-mini"),
maxOutputTokens: 1000,
prompt: message.text,
});
// Chat SDK handles progressive Slack message updates automatically
await thread.post(result.textStream);
});If using Bolt for JavaScript
Manually post an initial message and update it with streamed content:
import { streamText } from "ai";
import { gateway } from "@ai-sdk/gateway";
app.event('app_mention', async ({ event, client }) => {
const result = await streamText({
model: gateway("openai/gpt-4o-mini"),
maxOutputTokens: 1000,
prompt: event.text.replace(/<@[A-Z0-9]+>/g, '').trim(),
});
const msg = await client.chat.postMessage({
channel: event.channel,
thread_ts: event.thread_ts || event.ts,
text: "Thinking...",
});
let fullText = "";
for await (const chunk of result.textStream) {
fullText += chunk;
await client.chat.update({
channel: event.channel,
ts: msg.ts,
text: fullText,
});
}
});Tool Results in Slack
AI SDK tools work identically in both frameworks. The difference is how you post results:
If using Chat SDK
bot.onNewMention(async (thread, message) => {
const result = await generateText({
model: gateway("openai/gpt-4o-mini"),
tools: { /* your tools */ },
prompt: message.text,
});
await thread.post(result.text);
});If using Bolt for JavaScript
app.event('app_mention', async ({ event, client }) => {
const result = await generateText({
model: gateway("openai/gpt-4o-mini"),
tools: { /* your tools */ },
prompt: event.text.replace(/<@[A-Z0-9]+>/g, '').trim(),
});
await client.chat.postMessage({
channel: event.channel,
thread_ts: event.thread_ts || event.ts,
text: result.text,
});
});DevTools (Development Only)
Capture all AI SDK calls to a local JSON file for debugging:
pnpm add @ai-sdk/devtoolsimport { withDevTools } from "@ai-sdk/devtools";
const model = withDevTools(gateway("openai/gpt-4o-mini"));View captured calls at .devtools/generations.json or run npx @ai-sdk/devtools for a web UI at http://localhost:4983.
Best Practices
1. Verify APIs Against Source
CRITICAL: Always verify AI SDK APIs against the installed package - never rely on memory. The AI SDK evolves rapidly and training data may be outdated.
# Check bundled documentation
grep "generateText" node_modules/ai/docs/
# Check source code
grep "generateText" node_modules/ai/src/
# Check available functions
ls node_modules/ai/docs/2. Handle Errors Gracefully
try {
const result = await generateText({
model: gateway("openai/gpt-4o-mini"),
prompt: userMessage,
});
return result.text;
} catch (error) {
if (error.message.includes("rate_limit")) {
return "I'm receiving too many requests. Please try again in a moment.";
}
console.error("AI error:", error);
return "Sorry, I encountered an error processing your request.";
}3. Set Reasonable Limits
const result = await generateText({
model: gateway("openai/gpt-4o-mini"),
maxOutputTokens: 1000, // Limit response length
temperature: 0.7, // Control randomness
prompt: userMessage,
});4. Use Type-Safe Outputs
import { generateObject } from "ai";
import { z } from "zod";
const TaskSchema = z.object({
title: z.string(),
priority: z.enum(["low", "medium", "high"]),
dueDate: z.string().optional(),
});
const result = await generateObject({
model: gateway("openai/gpt-4o-mini"),
schema: TaskSchema,
prompt: "Create a task from: Review PR by end of day",
});
// result.object is fully typed
console.log(result.object.title);
console.log(result.object.priority);Model ID Discovery
CRITICAL: Never use model IDs from memory or training data. Model IDs change frequently. Always fetch current IDs before writing code.
Get current available models:
# OpenAI models
curl -s https://ai-gateway.vercel.sh/v1/models | jq -r '[.data[] | select(.id | startswith("openai/")) | .id] | reverse | .[]'
# Anthropic models
curl -s https://ai-gateway.vercel.sh/v1/models | jq -r '[.data[] | select(.id | startswith("anthropic/")) | .id] | reverse | .[]'
# Google models
curl -s https://ai-gateway.vercel.sh/v1/models | jq -r '[.data[] | select(.id | startswith("google/")) | .id] | reverse | .[]'Use the model with the highest version number for the latest capabilities.
References
Environment Variables Reference
Complete reference for all environment variables used in Slack agent projects.
Required Variables
SLACK_BOT_TOKEN
Description: OAuth token for authenticating Slack API calls. Auto-detected by @chat-adapter/slack (Chat SDK) or used with new App({ token }) (Bolt).
Source: 1. Go to https://api.slack.com/apps 2. Select your app 3. Navigate to Install App 4. Copy Bot User OAuth Token
Format: xoxb-XXXXXXXXX-XXXXXXXXX-XXXXXXXXXXXXXXXXXXXXXXXX
Security:
- Never commit to version control
- Rotate if compromised
- Use different tokens for dev/prod
---
SLACK_SIGNING_SECRET
Description: Secret used to verify requests originate from Slack. Auto-detected by @chat-adapter/slack (Chat SDK) or @vercel/slack-bolt (Bolt).
Source: 1. Go to https://api.slack.com/apps 2. Select your app 3. Navigate to Basic Information 4. Find Signing Secret under App Credentials
Format: 32-character hexadecimal string
Usage: Automatically used by the Chat SDK Slack adapter or @vercel/slack-bolt VercelReceiver to verify request signatures.
Security:
- Never commit to version control
- Rotate if compromised
- Each Slack app has a unique secret
---
REDIS_URL (Chat SDK only)
Description: Redis connection URL for the Chat SDK state adapter (@chat-adapter/state-redis). Not required for Bolt projects unless you add Redis manually.
Source:
- Upstash Redis (recommended for serverless)
- Any Redis-compatible provider
Format: redis://default:PASSWORD@HOST:PORT or rediss://... for TLS
Usage:
import { createRedisState } from "@chat-adapter/state-redis";
const state = createRedisState(); // Reads REDIS_URL automaticallyNote: For local development, you can use an in-memory state adapter instead:
import { createMemoryState } from "chat";
const state = createMemoryState();---
AI Integration
You have two options for AI/LLM integration:
Option 1: Vercel AI Gateway (Recommended)
When deployed on Vercel, the AI Gateway handles authentication automatically. No API keys needed!
For local development with AI Gateway:
To use AI Gateway locally (without API keys), you must link your project to Vercel:
1. Create a Vercel project: vercel 2. Link the project: vercel link 3. The OIDC token will now work locally
Without vercel link, you'll get authentication errors when using @ai-sdk/gateway.
import { generateText } from "ai";
import { gateway } from "@ai-sdk/gateway";
const result = await generateText({
model: gateway("openai/gpt-4o-mini"), // No API key needed!
prompt: "Hello world",
});Benefits:
- Zero configuration for API keys
- Access to multiple providers (OpenAI, Anthropic, Google, etc.)
- Built-in rate limiting and observability
- Works automatically on Vercel deployments
---
Option 2: Direct Provider SDK
If you prefer direct integration with a specific provider, you'll need to manage API keys yourself.
OPENAI_API_KEY
Description: API key for direct OpenAI integration.
Source: 1. Go to https://platform.openai.com/api-keys 2. Create a new API key 3. Copy the key (starts with sk-)
Setup:
pnpm add @ai-sdk/openaiUsage:
import { generateText } from "ai";
import { openai } from "@ai-sdk/openai";
const result = await generateText({
model: openai("gpt-4o-mini"),
prompt: "Hello world",
});---
ANTHROPIC_API_KEY
Description: API key for direct Anthropic integration.
Source: 1. Go to https://console.anthropic.com/settings/keys 2. Create a new API key 3. Copy the key (starts with sk-ant-)
Setup:
pnpm add @ai-sdk/anthropicUsage:
import { generateText } from "ai";
import { anthropic } from "@ai-sdk/anthropic";
const result = await generateText({
model: anthropic("claude-sonnet-4-20250514"),
prompt: "Hello world",
});---
GOOGLE_GENERATIVE_AI_API_KEY
Description: API key for direct Google AI integration.
Source: 1. Go to https://aistudio.google.com/apikey 2. Create a new API key 3. Copy the key
Setup:
pnpm add @ai-sdk/googleUsage:
import { generateText } from "ai";
import { google } from "@ai-sdk/google";
const result = await generateText({
model: google("gemini-2.0-flash"),
prompt: "Hello world",
});---
Development-Only Variables
NGROK_AUTH_TOKEN
Description: Authentication token for ngrok tunneling service.
Source: 1. Go to https://dashboard.ngrok.com 2. Sign up or log in 3. Navigate to Your Authtoken 4. Copy the token
Format: Alphanumeric string
Usage: Used for local development tunneling.
Note: Not needed in production deployments.
---
Optional Variables
NODE_ENV
Description: Node.js environment indicator.
Values:
development- Local developmentproduction- Production deploymenttest- Test environment
Default: development locally, production on Vercel
---
LOG_LEVEL
Description: Controls logging verbosity.
Values:
debug- All logs including debug infoinfo- Standard operational logswarn- Warnings and errors onlyerror- Errors only
Default: info
---
Local Development Setup
Create a .env file in your project root.
If using Chat SDK (with Vercel AI Gateway)
# Required - Slack credentials (auto-detected by Chat SDK)
SLACK_BOT_TOKEN=xoxb-your-token-here
SLACK_SIGNING_SECRET=your-signing-secret
# Required - State persistence
REDIS_URL=redis://default:password@host:port
# Development tunnel
NGROK_AUTH_TOKEN=your-ngrok-token
# Optional
NODE_ENV=development
LOG_LEVEL=debug
# No AI keys needed - Vercel AI Gateway handles this automatically!If using Bolt for JavaScript (with Vercel AI Gateway)
# Required - Slack credentials
SLACK_BOT_TOKEN=xoxb-your-token-here
SLACK_SIGNING_SECRET=your-signing-secret
# Development tunnel
NGROK_AUTH_TOKEN=your-ngrok-token
# Optional
NODE_ENV=development
LOG_LEVEL=debug
# No AI keys needed - Vercel AI Gateway handles this automatically!
# No REDIS_URL needed unless you add Redis manuallyUsing Direct Provider SDK (either framework)
Add your provider's API key:
# AI Provider API Key (choose one based on your provider)
OPENAI_API_KEY=sk-your-openai-key
# ANTHROPIC_API_KEY=sk-ant-your-anthropic-key
# GOOGLE_GENERATIVE_AI_API_KEY=your-google-keySecurity Best Practices
1. Never Commit Secrets
Ensure .gitignore includes:
.env
.env.local
.env.*.local2. Use Different Credentials Per Environment
| Environment | Slack App | Tokens |
|---|---|---|
| Development | Dev App | Dev tokens |
| Staging | Staging App | Staging tokens |
| Production | Prod App | Prod tokens |
3. Rotate Compromised Credentials
If a secret is exposed:
For Slack tokens: 1. Go to app settings > Install App 2. Click Reinstall App 3. Update all environment variables
For Signing Secret: 1. Go to Basic Information 2. Click Regenerate under Signing Secret 3. Update all environment variables
4. Limit Token Scopes
Only request the OAuth scopes your app needs. Review scopes in your manifest.json.
5. Monitor Usage
- Check Slack app analytics for unusual activity
- Monitor Vercel function logs for errors
- Set up alerts for anomalies
Vercel Environment Configuration
Setting Variables
Via Dashboard: 1. Project Settings > Environment Variables 2. Add variable name and value 3. Select environments (Production/Preview/Development) 4. Save and redeploy
Via CLI:
vercel env add VARIABLE_NAME productionEnvironment Scopes
| Scope | When Used |
|---|---|
| Production | vercel --prod deployments |
| Preview | Pull request deployments |
| Development | vercel dev local server |
Sensitive vs Non-Sensitive
Mark variables as Sensitive for:
- API keys
- Tokens
- Secrets
Sensitive variables:
- Are encrypted at rest
- Don't appear in logs
- Can't be read via API
Accessing Variables
In Server Code
// Direct access
const token = process.env.SLACK_BOT_TOKEN;
// With validation
function getRequiredEnv(name: string): string {
const value = process.env[name];
if (!value) {
throw new Error(`Missing required environment variable: ${name}`);
}
return value;
}
const token = getRequiredEnv('SLACK_BOT_TOKEN');In Framework Config
If using Chat SDK (Next.js)
// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
// Environment variables are available via process.env in server components and API routes
};
export default nextConfig;If using Bolt for JavaScript (Nitro)
// nitro.config.ts
export default defineNitroConfig({
runtimeConfig: {
slackBotToken: process.env.SLACK_BOT_TOKEN,
slackSigningSecret: process.env.SLACK_SIGNING_SECRET,
},
});// Access via useRuntimeConfig()
const config = useRuntimeConfig();
const token = config.slackBotToken;Troubleshooting
Variable Not Found
Symptoms: undefined value, missing env error
Solutions: 1. Check variable name spelling (case-sensitive) 2. Verify .env file is in project root 3. Restart dev server after changes 4. Redeploy after adding to Vercel
Invalid Token Errors
Symptoms: invalid_auth, token_revoked
Solutions: 1. Verify token is complete (no truncation) 2. Check for extra whitespace 3. Confirm token matches the workspace 4. Regenerate if expired/revoked
Signature Verification Failed
Symptoms: invalid_signature errors
Solutions: 1. Verify signing secret is correct 2. Check for request timestamp issues 3. Ensure secret matches the Slack app
Slack App Setup Guide
Complete guide for creating and configuring a Slack app for your agent.
Prerequisites
- A Slack workspace where you have permission to install apps
- Access to https://api.slack.com
- Your project's
manifest.jsonfile
Step 1: Create the Slack App
1. Go to https://api.slack.com/apps/new 2. Select "From an app manifest" 3. Choose your target workspace from the dropdown 4. Click Next
Configure the Manifest
5. Switch to the JSON tab 6. Delete any existing content 7. Paste the contents of your project's manifest.json
The webhook URL depends on your framework:
- Chat SDK:
https://your-domain.vercel.app/api/webhooks/slack - Bolt for JavaScript:
https://your-domain.vercel.app/api/slack/events
Sample Manifest (Chat SDK)
{
"display_information": {
"name": "Your Slack Agent",
"description": "AI-powered assistant",
"background_color": "#000000"
},
"features": {
"app_home": {
"home_tab_enabled": true,
"messages_tab_enabled": true,
"messages_tab_read_only_enabled": false
},
"bot_user": {
"display_name": "Your Agent",
"always_online": true
}
},
"oauth_config": {
"scopes": {
"bot": [
"channels:history",
"channels:read",
"chat:write",
"commands",
"app_mentions:read",
"groups:history",
"im:history",
"mpim:history",
"assistant:write",
"reactions:read",
"reactions:write",
"channels:join"
]
}
},
"settings": {
"event_subscriptions": {
"request_url": "https://your-domain.vercel.app/api/webhooks/slack",
"bot_events": [
"app_mention",
"assistant_thread_started",
"assistant_thread_context_changed",
"message.channels",
"message.groups",
"message.im",
"message.mpim",
"reaction_added"
]
},
"interactivity": {
"is_enabled": true,
"request_url": "https://your-domain.vercel.app/api/webhooks/slack"
},
"org_deploy_enabled": false,
"socket_mode_enabled": false,
"token_rotation_enabled": false
}
}Sample Manifest (Bolt for JavaScript)
Uses the same structure, but with a different webhook URL path:
{
"settings": {
"event_subscriptions": {
"request_url": "https://your-domain.vercel.app/api/slack/events",
"bot_events": ["app_mention", "assistant_thread_started", "assistant_thread_context_changed", "message.channels", "message.groups", "message.im", "message.mpim"]
},
"interactivity": {
"is_enabled": true,
"request_url": "https://your-domain.vercel.app/api/slack/events"
}
}
}Note: The Chat SDK manifest includes reactions:read and reactions:write scopes for the onReaction handler. Add these to Bolt projects if you handle reaction events.
8. Click Next 9. Review the permissions and click Create
Step 2: Install to Workspace
1. In your new app's dashboard, navigate to Install App in the sidebar 2. Click Install to Workspace 3. Review the permissions request 4. Click Allow
Step 3: Get Your Credentials
Bot User OAuth Token
1. Go to Install App in the sidebar 2. Find Bot User OAuth Token 3. Copy the token (starts with xoxb-)
Security: Never commit this token to version control.
Signing Secret
1. Go to Basic Information in the sidebar 2. Scroll to App Credentials 3. Find Signing Secret 4. Click Show and copy the value
Step 4: Configure URLs (Production)
For production deployments, update the URLs in your manifest and re-upload it:
Check for Deployment Protection
If your Vercel project has Deployment Protection enabled, you'll need a bypass secret:
1. Go to Vercel Dashboard -> Project Settings -> Deployment Protection 2. Under "Protection Bypass for Automation", copy the secret 3. Add it as a query parameter to your URLs:
- Chat SDK:
https://your-app.vercel.app/api/webhooks/slack?x-vercel-protection-bypass=YOUR_SECRET - Bolt:
https://your-app.vercel.app/api/slack/events?x-vercel-protection-bypass=YOUR_SECRET
Update the Manifest
1. Edit your project's manifest.json 2. Update the URLs in these fields (using your framework's webhook path):
{
"settings": {
"event_subscriptions": {
"request_url": "https://your-app.vercel.app/api/webhooks/slack"
},
"interactivity": {
"request_url": "https://your-app.vercel.app/api/webhooks/slack"
}
}
}Chat SDK uses /api/webhooks/slack. Bolt uses /api/slack/events. 3. Go to your app at https://api.slack.com/apps 4. Navigate to App Manifest in the sidebar 5. Switch to the JSON tab 6. Replace the manifest with your updated version 7. Click Save Changes
This approach is more reliable than manually editing Event Subscriptions and Interactivity pages separately.
Step 5: Local Development Setup
For local development, use ngrok to create a tunnel:
# Start your dev server
pnpm dev
# In a separate terminal, expose your local server
ngrok http 3000Then update your Slack app's manifest with the ngrok URL:
- Chat SDK:
https://abc123.ngrok.io/api/webhooks/slack - Bolt:
https://abc123.ngrok.io/api/slack/events
Troubleshooting
"url_verification" failed
Cause: Slack couldn't verify your endpoint.
Solutions: 1. Ensure your server is running 2. Check the URL is correct and accessible (Chat SDK: /api/webhooks/slack, Bolt: /api/slack/events) 3. Verify your endpoint returns the challenge parameter correctly (both frameworks handle this automatically) 4. If using Vercel with Deployment Protection, add the bypass secret to your URL (see Step 4)
"invalid_auth" errors
Cause: Bot token is invalid or expired.
Solutions: 1. Regenerate the token in Install App 2. Verify you're using the correct token (not a user token) 3. Check the token hasn't been revoked
Events not being received
Cause: Event subscriptions not configured correctly.
Solutions: 1. Verify Enable Events is toggled On 2. Check the Request URL is correct (Chat SDK: /api/webhooks/slack, Bolt: /api/slack/events) 3. Ensure all required bot events are subscribed 4. Check your server logs for incoming requests
"channel_not_found" when joining channels
Cause: Bot can't access private channels.
Solutions: 1. The bot can only join public channels 2. For private channels, a user must invite the bot 3. Check the channel ID is correct
Rate limiting
Cause: Too many API requests.
Solutions: 1. Implement exponential backoff 2. Cache responses where appropriate 3. Batch operations when possible
Required Bot Scopes Explained
| Scope | Purpose |
|---|---|
channels:history | Read messages in public channels |
channels:read | View basic channel info |
chat:write | Send messages as the bot |
commands | Handle slash commands |
app_mentions:read | Receive @mention events |
groups:history | Read messages in private channels (if invited) |
im:history | Read direct message history |
mpim:history | Read group DM history |
assistant:write | Use Slack's Assistant features |
reactions:read | Receive reaction events (for onReaction handler) |
reactions:write | Add emoji reactions |
channels:join | Join public channels |
Next Steps
After setup: 1. Add the bot to a channel: /invite @YourBotName 2. Mention the bot: @YourBotName hello 3. Check your server logs to verify events are received
Vercel Deployment Guide
Complete guide for deploying your Slack agent to Vercel.
Prerequisites
- A Vercel account (https://vercel.com)
- Your project pushed to a Git repository (GitHub, GitLab, or Bitbucket)
- Slack app credentials from the Slack setup
Deployment Options
Option A: Deploy via Vercel Dashboard (Recommended)
1. Go to https://vercel.com/new 2. Click Import Git Repository 3. Select your repository 4. Configure project settings:
- Chat SDK: Framework Preset = Next.js, Output Directory =
.next(auto-detected) - Bolt: Framework Preset = Other, Output Directory =
.output - Root Directory: Leave as default
- Build Command:
pnpm build(or leave default)
5. Click Deploy
Option B: Deploy via CLI
# Install Vercel CLI
npm i -g vercel
# Login (if not already)
vercel login
# Deploy
vercel
# For production
vercel --prodConfigure Environment Variables
After initial deployment, configure your environment variables.
Via Dashboard
1. Go to your project in Vercel Dashboard 2. Navigate to Settings > Environment Variables 3. Add the following variables:
| Variable | Value | Environments | Required By |
|---|---|---|---|
SLACK_BOT_TOKEN | xoxb-your-token | Production, Preview | Both |
SLACK_SIGNING_SECRET | your-signing-secret | Production, Preview | Both |
REDIS_URL | redis://... | Production, Preview | Chat SDK only |
4. Click Save for each variable 5. Redeploy your project to apply the changes
Via CLI
# Add environment variable
vercel env add SLACK_BOT_TOKEN production
# You'll be prompted to enter the valueUpdate Slack App URLs
After deployment, update your Slack app to use the production URL.
1. Get your Vercel deployment URL (e.g., https://your-app.vercel.app) 2. Go to https://api.slack.com/apps 3. Select your production Slack app 4. Update these locations:
Event Subscriptions
1. Go to Event Subscriptions 2. Update Request URL to your framework's webhook path:
- Chat SDK:
https://your-app.vercel.app/api/webhooks/slack - Bolt:
https://your-app.vercel.app/api/slack/events
3. Wait for verification (green checkmark) 4. Click Save Changes
Interactivity & Shortcuts
1. Go to Interactivity & Shortcuts 2. Update Request URL (same path as Event Subscriptions above) 3. Click Save Changes
Slash Commands (if using)
1. Go to Slash Commands 2. Edit each command 3. Update Request URL (same path as Event Subscriptions above) 4. Click Save
Verify Deployment
1. Open your Slack workspace 2. Find your bot (it should show as online) 3. Send a test message: @YourBot hello 4. Check Vercel logs for the request:
- Dashboard > Your Project > Logs
Automatic Deployments
Once connected to Git, Vercel automatically deploys:
- Production: On push to
mainbranch - Preview: On push to other branches
Preview Deployments
Each pull request gets a unique preview URL. Note:
- Preview deployments use Preview environment variables
- Slack webhooks won't work on preview URLs unless configured
Function Configuration
For long-running agent operations, configure function settings in vercel.json:
Chat SDK (Next.js):
{
"functions": {
"app/api/webhooks/[platform]/route.ts": {
"maxDuration": 30
}
}
}Bolt (Nitro):
{
"functions": {
"api/slack/events.ts": {
"maxDuration": 30
}
}
}Note: Free tier has 10s limit, Pro tier allows up to 60s.
Monitoring
Vercel Dashboard
- Deployments: View deployment history and status
- Logs: Real-time function logs
- Analytics: Request metrics and performance
Recommended Logging
Add logging to your agent for debugging:
console.log('[Slack Event]', event.type, {
user: event.user,
channel: event.channel,
});Logs appear in Vercel Dashboard > Logs.
Troubleshooting
504 Gateway Timeout
Cause: Function exceeded time limit.
Solutions: 1. Increase maxDuration in vercel.json 2. Upgrade to Pro tier for longer limits 3. Optimize your agent's response time 4. Stream responses instead of waiting for completion
Environment Variables Not Working
Cause: Variables not available after adding.
Solutions: 1. Redeploy after adding variables 2. Check variable scope (Production/Preview/Development) 3. Verify variable names match exactly (case-sensitive)
Webhook Verification Failed
Cause: Slack can't verify your endpoint.
Solutions: 1. Ensure deployment is complete 2. Check the URL is exactly correct (Chat SDK: /api/webhooks/slack, Bolt: /api/slack/events) 3. Verify your endpoint handles the challenge correctly (both frameworks do this automatically) 4. Check Vercel logs for errors
Cold Start Delays
Cause: Serverless function warming up.
Solutions: 1. Use Vercel's Edge Functions where possible 2. Optimize bundle size 3. Consider Vercel's Pro tier for reduced cold starts
Production Checklist
Before going live:
- [ ] Environment variables configured for Production
- [ ] Slack Request URLs updated to production domain (Chat SDK:
/api/webhooks/slack, Bolt:/api/slack/events) - [ ] Webhook verification successful
- [ ] Test message/mention working
- [ ] Error handling in place
- [ ] Logging configured for debugging
- [ ] Rate limiting considered
- [ ] Separate production Slack app from development
Rollback
If something goes wrong:
1. Go to Deployments in Vercel Dashboard 2. Find a working previous deployment 3. Click the ... menu 4. Select Promote to Production
This instantly reverts to the previous version.
/**
* Unit tests for Slack Agent
*
* Copy this template to server/lib/ai/agent.test.ts and customize
* for your specific agent implementation.
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
// Import your agent creation function
// import { createSlackAgent } from './agent';
// Mock dependencies
vi.mock('ai', () => ({
tool: vi.fn((config) => config),
generateText: vi.fn(),
streamText: vi.fn(),
}));
describe('createSlackAgent', () => {
// Sample context matching slack-agent-template structure
const mockContext = {
channel_id: 'C12345678',
dm_channel: 'D12345678',
thread_ts: '1234567890.123456',
is_dm: false,
team_id: 'T12345678',
};
beforeEach(() => {
vi.clearAllMocks();
});
describe('agent creation', () => {
it('should create agent with required properties', () => {
// TODO: Uncomment and adapt to your implementation
// const agent = createSlackAgent(mockContext);
//
// expect(agent).toBeDefined();
// expect(agent.model).toBeDefined();
// expect(agent.tools).toBeDefined();
// expect(agent.system).toBeDefined();
expect(true).toBe(true); // Placeholder
});
it('should include context information in system prompt', () => {
// TODO: Adapt to your implementation
// const agent = createSlackAgent(mockContext);
//
// expect(agent.system).toContain(mockContext.channel_id);
expect(true).toBe(true); // Placeholder
});
it('should configure correct model', () => {
// TODO: Adapt to your implementation
// const agent = createSlackAgent(mockContext);
//
// expect(agent.model).toBe('expected-model-name');
expect(true).toBe(true); // Placeholder
});
});
describe('context handling', () => {
it('should handle channel context', () => {
const channelContext = {
...mockContext,
is_dm: false,
channel_id: 'C12345678',
};
// TODO: Test channel-specific behavior
// const agent = createSlackAgent(channelContext);
// expect(agent.system).toContain('channel');
expect(channelContext.is_dm).toBe(false);
});
it('should handle DM context', () => {
const dmContext = {
...mockContext,
is_dm: true,
channel_id: '',
dm_channel: 'D12345678',
};
// TODO: Test DM-specific behavior
// const agent = createSlackAgent(dmContext);
// expect(agent.system).toContain('direct message');
expect(dmContext.is_dm).toBe(true);
});
it('should handle thread context', () => {
const threadContext = {
...mockContext,
thread_ts: '1234567890.123456',
};
// TODO: Test thread-specific behavior
// const agent = createSlackAgent(threadContext);
// expect(agent).toBeDefined();
expect(threadContext.thread_ts).toBeDefined();
});
it('should handle missing optional context', () => {
const minimalContext = {
channel_id: 'C12345678',
team_id: 'T12345678',
is_dm: false,
};
// TODO: Test with minimal context
// const agent = createSlackAgent(minimalContext);
// expect(agent).toBeDefined();
expect(minimalContext).toBeDefined();
});
});
describe('tool configuration', () => {
it('should include required Slack tools', () => {
// TODO: Adapt to your implementation
// const agent = createSlackAgent(mockContext);
// const toolNames = Object.keys(agent.tools || {});
//
// expect(toolNames).toContain('getChannelMessages');
// expect(toolNames).toContain('getThreadMessages');
// expect(toolNames).toContain('joinChannel');
// expect(toolNames).toContain('searchChannels');
expect(true).toBe(true); // Placeholder
});
it('should not include restricted tools in DM', () => {
const dmContext = { ...mockContext, is_dm: true };
// TODO: Test tool restrictions
// const agent = createSlackAgent(dmContext);
// const toolNames = Object.keys(agent.tools || {});
//
// Certain tools might be restricted in DMs
expect(dmContext.is_dm).toBe(true);
});
});
describe('error handling', () => {
it('should handle invalid context gracefully', () => {
// TODO: Test error handling
// expect(() => createSlackAgent(null)).toThrow();
// expect(() => createSlackAgent({})).toThrow();
expect(true).toBe(true); // Placeholder
});
});
});
Related skills
How it compares
Use slack-agent for guided Vercel Slack bot setup; use raw Bolt or Chat SDK docs when you already have OAuth and deployment wired.
FAQ
What does slack-agent do?
Use when working on Slack agent/bot code, Chat SDK applications, Bolt for JavaScript projects, or projects using @chat-adapter/slack or @slack/bolt. Provides development patterns, testing...
When should I use slack-agent?
Invoke when Use when working on Slack agent/bot code, Chat SDK applications, Bolt for JavaScript projects, or projects using @chat-adapter/slack or @sla.
Is slack-agent safe to install?
Review the Security Audits panel on this page before installing in production.