Vercel Logo

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:

  1. User Intent: "@bot add thumbs up" → AI detects reaction intent
  2. Tool Selection: AI chooses reactToMessageTool from available tools
  3. Parameter Validation: Zod schema validates { emoji: "thumbs_up" }
  4. Slack API Call: Tool executes app.client.reactions.add()
  5. Result Processing: Tool returns success/error message
  6. 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

  1. Create new tool file: /slack-agent/server/lib/ai/tools/react-to-message.ts
  2. Export from tools index and add to schema
  3. 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.client for Slack API calls

Hands-On Exercise 4.2

Extend your bot with tools that take actions in Slack:

Requirements:

  1. Create reactToMessageTool in /slack-agent/server/lib/ai/tools/react-to-message.ts
  2. Use Zod schema to validate emoji parameter
  3. Add the tool to availableToolsSchema and export it
  4. Wire it into respond-to-message.ts
  5. Update getActiveTools so reactToMessageTool is only available when there is message context (threads and app mentions)
  6. Handle errors gracefully (invalid emoji, missing permissions)

Implementation hints:

  • Tools use the tool function 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

  1. Test basic reaction:

    @bot add a thumbs up to this message
    

    Bot adds 👍 reaction and responds: "👍 Got it."

  2. Test "watch" intent:

    @bot watch this thread for updates
    

    Bot adds 👀 reaction and responds: "👀 Watching this thread."

  3. 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 message
    

    These fields (event_id, ts, thread_ts) come from context.correlation set up in Bolt Middleware — you should spread ...context.correlation into your tool execution logs to make them traceable.

Troubleshooting

Error → User Message Mapping

Slack ErrorUser SeesCommon 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:

/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:

/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:

/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 inputSchema to 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_reacted error 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.