Give your AI tools to take actions beyond text responses
Without tools, your bot just talks. With tools, it acts—adding reactions when users say "watch this thread", searching past conversations, creating tickets. The difference between a chatbot and an assistant that actually gets shit done.
How Tools Work
┌─────────────────────────────────────────────────────────────────┐
│ AI Tool Execution Flow │
└─────────────────────────────────────────────────────────────────┘
User Message: "@bot add thumbs up"
↓
┌─────────────────┐ Intent ┌─────────────────┐
│ AI Model │ Detection │ Tool Selection │
│ (gpt-4o-mini) │ ──────────────→│ reactToMessage │
└─────────────────┘ │ Tool │
↑ └─────────────────┘
│ ↓ execute()
│ ┌─────────────────┐
│ │ Zod Validation │
│ │ { emoji: "👍" } │
│ └─────────────────┘
│ ↓
│ ┌─────────────────┐
│ │ Slack API │
│ │ reactions.add() │
│ └─────────────────┘
│ ↓
│ Tool Result ┌─────────────────┐
└───────────────────────── │ 👍 Added to │
│ Message │
└─────────────────┘
↓
Final AI Response:
"Added thumbs up reaction"
Tool Execution Steps:
- User Intent: "@bot add thumbs up" → AI detects reaction intent
- Tool Selection: AI chooses
reactToMessageToolfrom available tools - Parameter Validation: Zod schema validates
{ emoji: "thumbs_up" } - Slack API Call: Tool executes
app.client.reactions.add() - Result Processing: Tool returns success/error message
- AI Response: Final message confirms action: "Added thumbs up reaction"
Outcome
Implement AI tools using Zod schemas that enable your bot to take actions in Slack.
Fast Track
- Create new tool file:
/slack-agent/server/lib/ai/tools/react-to-message.ts - Export from tools index and add to schema
- Test: "@bot add eyes emoji to watch this"
Building on Previous Lessons
This lesson extends the foundation from previous sections:
- From system prompts: System prompts now trigger tool usage based on user intent
- From repository flyover: Tools receive context from the same utilities (
getThreadMessages,getChannelMessages) - From Bolt Middleware: Correlation logging makes tool executions traceable in logs
- From template: Uses the existing
app.clientfor Slack API calls
Hands-On Exercise 4.2
Extend your bot with tools that take actions in Slack:
Requirements:
- Create
reactToMessageToolin/slack-agent/server/lib/ai/tools/react-to-message.ts - Use Zod schema to validate emoji parameter
- Add the tool to
availableToolsSchemaand export it - Wire it into
respond-to-message.ts - Update
getActiveToolssoreactToMessageToolis only available when there is message context (threads and app mentions) - Handle errors gracefully (invalid emoji, missing permissions)
Implementation hints:
- Tools use the
toolfunction from Vercel AI SDK - Access Slack context via
experimental_context - Use
app.client.reactions.add()for the Slack API call - Common emojis: eyes, white_check_mark, rocket, thinking_face
Zod schema definition:
inputSchema: z.object({
emoji: z.string().describe("The emoji to add (without colons)"),
})Try It
-
Test basic reaction:
@bot add a thumbs up to this messageBot adds 👍 reaction and responds: "👍 Got it."
-
Test "watch" intent:
@bot watch this thread for updatesBot adds 👀 reaction and responds: "👀 Watching this thread."
-
Real logs showing tool execution:
[INFO] bolt-app { event_id: 'Ev09EKNCBGR5', ts: '1734567890.123456', thread_ts: '1734567890.123456', operation: 'toolCall', tool: 'reactToMessageTool' } Processing tool execution [DEBUG] tool call args: [ { emoji: 'eyes' } ] [DEBUG] web-api:WebClient:0 apiCall('reactions.add') start [INFO] bolt-app { event_id: 'Ev09EKNCBGR5', ts: '1734567890.123456', thread_ts: '1734567890.123456', tool: 'reactToMessageTool', result: 'success' } Added reaction :eyes: to messageThese fields (
event_id,ts,thread_ts) come fromcontext.correlationset up in Bolt Middleware — you should spread...context.correlationinto your tool execution logs to make them traceable.
Troubleshooting
Error → User Message Mapping
| Slack Error | User Sees | Common Cause |
|---|---|---|
already_reacted | "Already reacted with [emoji]" | Bot already added this emoji |
invalid_name | "Unknown emoji: [emoji]" | Emoji doesn't exist in workspace |
no_reaction | "Cannot use emoji: [emoji]" | Missing permissions |
not_in_channel | "Could not add reaction" | Bot not in channel |
Tool not being called:
- Check tool is exported from
/tools/index.ts - Verify it's added to
availableToolsSchema - Ensure it's included in the tools object in
respond-to-message.ts
"already_reacted" error:
- Bot already added that emoji
- Handle gracefully:
if (error.data?.error === 'already_reacted')
"no_permission" error:
- Bot lacks reactions:write scope
- Check manifest.json includes the scope
Commit
git add -A
git commit -m "feat(ai): add react-to-message tool for emoji reactions"Done-When
- Bot adds reactions when asked ("add thumbs up")
- Tool validates emoji parameter with Zod
- Errors handled gracefully (already reacted, invalid emoji)
- Tool receives Slack context (channel, timestamp)
Solution
Create /slack-agent/server/lib/ai/tools/react-to-message.ts:
import { tool } from "ai";
import { z } from "zod";
import { app } from "~/app";
import type { ExperimentalContext } from "../respond-to-message";
export const reactToMessageTool = tool({
description: "Add an emoji reaction to the current message",
inputSchema: z.object({
emoji: z.string().describe("The emoji name (without colons)"),
}),
execute: async ({ emoji }, { experimental_context }) => {
const context = experimental_context as ExperimentalContext;
if (!context?.channel || !context?.thread_ts) {
throw new Error("Missing Slack context");
}
// Clean emoji name: remove colons, convert spaces to underscores
const cleanEmoji = emoji
.toLowerCase()
.replace(/:/g, '')
.replace(/\s+/g, '_');
try {
await app.client.reactions.add({
channel: context.channel,
timestamp: context.thread_ts,
name: cleanEmoji,
});
return `Added ${cleanEmoji} reaction`;
} catch (error) {
// Handle Slack API errors with proper types
if (error && typeof error === 'object' && 'data' in error) {
const slackError = error as { data?: { error?: string } };
switch (slackError.data?.error) {
case 'already_reacted':
return `Already reacted with ${emoji}`;
case 'invalid_name':
return `Unknown emoji: ${emoji}`;
case 'no_reaction':
return `Cannot use emoji: ${emoji}`;
default:
app.logger.error('Reaction failed:', error);
return `Could not add reaction`;
}
}
throw error;
}
},
});Update /slack-agent/server/lib/ai/tools/index.ts:
export * from "./get-channel-messages";
export * from "./get-thread-messages";
export * from "./update-agent-status";
export * from "./update-chat-title";
export * from "./react-to-message"; // Add this
import type { KnownEventFromType } from "@slack/bolt";
import { z } from "zod";
import { app } from "~/app";
export const availableToolsSchema = z.enum([
"getChannelMessagesTool",
"getThreadMessagesTool",
"updateAgentStatusTool",
"updateChatTitleTool",
"reactToMessageTool", // Add this
]);Update /slack-agent/server/lib/ai/respond-to-message.ts:
import {
getActiveTools,
getChannelMessagesTool,
getThreadMessagesTool,
updateAgentStatusTool,
updateChatTitleTool,
reactToMessageTool, // Add this import
} from "./tools";
// ... in the existing streamText call:
tools: {
updateChatTitleTool,
getThreadMessagesTool,
getChannelMessagesTool,
updateAgentStatusTool,
reactToMessageTool, // Add the tool here
},
experimental_context: {
channel,
thread_ts: thread_ts || event.ts, // Use message ts if no thread
botId,
},Key implementation details:
- Uses
inputSchemato define parameters with Zod - Tool receives Slack context via
experimental_context - Emoji names need cleaning (spaces → underscores)
- Tool returns short, human-friendly confirmations (e.g., "👍 Got it.", "👀 Watching this thread.")
- Handle
already_reactederror gracefully
Advanced: Tool Availability Control
The template uses getActiveTools in /tools/index.ts to control which tools are available based on context. The react tool is available:
- In threads (when there's a
thread_ts) - For app mentions (when the bot is @mentioned)
// From getActiveTools - react tool availability
if (hasThread) {
tools.add("reactToMessageTool"); // Can react in threads
} else if (isAppMention) {
tools.add("reactToMessageTool"); // Can react to mentions
}This prevents the bot from trying to add reactions when there's no message context.
Was this helpful?