---
title: "AI Tools and Functions"
description: "Learn how AI tools extend your bot's capabilities beyond text responses. Implement tools with Zod schemas that let AI react to messages, fetch summaries, and interact with Slack APIs based on user intent."
canonical_url: "https://vercel.com/academy/slack-agents/ai-tools-and-functions"
md_url: "https://vercel.com/academy/slack-agents/ai-tools-and-functions.md"
docset_id: "vercel-academy"
doc_version: "1.0"
last_updated: "2026-04-09T09:43:36.172Z"
content_type: "lesson"
course: "slack-agents"
course_title: "Slack Agents on Vercel with the AI SDK"
prerequisites:  []
---

<agent-instructions>
Vercel Academy — structured learning, not reference docs.
Lessons are sequenced.
Adapt commands to the human's actual environment (OS, package manager, shell, editor) — detect from project context or ask, don't assume.
The lesson shows one path; if the human's project diverges, adapt concepts to their setup.
Preserve the learning goal over literal steps.
Quizzes are pedagogical — engage, don't spoil.
Quiz answers are included for your reference.
</agent-instructions>

# AI Tools and Functions

# 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-shape-behavior)**: System prompts now trigger tool usage based on user intent
- **From [repository flyover](./repository-flyover)**: Tools receive context from the same utilities (`getThreadMessages`, `getChannelMessages`)
- **From [Bolt Middleware](./bolt-nitro-middleware-and-logging)**: 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:**

```typescript
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](./bolt-nitro-middleware-and-logging) — you should spread `...context.correlation` into 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

```bash
git add -A
git commit -m "feat(ai): add react-to-message tool for emoji reactions"
```

## Done-When

- [x] Bot adds reactions when asked ("add thumbs up")
- [x] Tool validates emoji parameter with Zod
- [x] Errors handled gracefully (already reacted, invalid emoji)
- [x] Tool receives Slack context (channel, timestamp)

## Solution

Create `/slack-agent/server/lib/ai/tools/react-to-message.ts`:

```typescript title="/slack-agent/server/lib/ai/tools/react-to-message.ts" {8-10,18-22,25-27,32-35}
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`:

```typescript title="/slack-agent/server/lib/ai/tools/index.ts" {5,15-16}
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`:

```typescript title="/slack-agent/server/lib/ai/respond-to-message.ts" {7,16,19-21}
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)

```typescript
// 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.


---

[Full course index](/academy/llms.txt) · [Sitemap](/academy/sitemap.md)
