---
title: AI SDK Tools
description: Give an AI agent the ability to operate inside your workspace. Post messages, send DMs, react, edit, delete; all with built-in approval gates.
type: guide
prerequisites:
  - /docs/usage
related:
  - /docs/ai
  - /docs/ai/to-ai-messages
  - /docs/streaming
  - /docs/conversation-history
  - /docs/approvals
---

# AI SDK Tools



`createChatTools` exposes Chat SDK operations as ready-to-use [AI SDK](https://ai-sdk.dev) tools so an agent can act inside the same workspaces your bot is connected to: read messages, post replies, send DMs, react, edit, delete, and manage thread subscriptions across every adapter you've registered.

Write operations require user approval out of the box, toggle them globally or per-tool when you want unattended execution.

## Installation

The tools live in the [`chat/ai`](/docs/ai) subpath of the core `chat` package:

```ts
import { createChatTools } from "chat/ai";
```

`ai` and `zod` are optional peer dependencies — install them if you haven't already:

<PackageInstall package="ai zod" />

<Callout>
  Pair `createChatTools` with [`toAiMessages`](/docs/ai/to-ai-messages)
  to feed prior thread history into the agent before it picks a tool.
  Both ship from the same `chat/ai` subpath, which keeps the optional
  `ai` / `zod` peer deps out of bundles that don't import them.
</Callout>

## Quick start

Pass your `Chat` instance and the tools you want into any AI SDK call:

```typescript title="lib/agent.ts" lineNumbers
import { Chat } from "chat";
import { createChatTools } from "chat/ai";
import { createSlackAdapter } from "@chat-adapter/slack";
import { createMemoryState } from "@chat-adapter/state-memory";
import { generateText } from "ai";

const chat = new Chat({
  userName: "mybot",
  adapters: { slack: createSlackAdapter() },
  state: createMemoryState(),
});

const result = await generateText({
  model: "xai/grok-4.5",
  tools: createChatTools({
    chat,
    preset: "messenger",
    requireApproval: false, // unattended script, no human-in-the-loop needed
  }),
  prompt:
    "Post a friendly hello in slack:C0123ABC and react to it with a thumbs up.",
});
```

Each tool resolves the right adapter from the id prefix you give it (`slack:...`, `discord:...`, `gchat:...`), so the same agent can drive any platform your `Chat` instance is wired up to.

## Presets

Pass `preset` to scope the toolset down to what an agent actually needs.

```typescript
// Read-only — fetch messages, threads, channel info, users
createChatTools({ chat, preset: "reader" });

// Basic posting — read + post + DM + react + typing indicator
createChatTools({ chat, preset: "messenger" });

// Full management — everything including edit, delete, subscriptions
createChatTools({ chat, preset: "moderator" });
```

Presets compose — pass an array to combine them:

```typescript
createChatTools({ chat, preset: ["reader", "messenger"] });
```

Omit `preset` entirely to get every tool (same as `'moderator'`).

| Preset      | Tools included                                                                                                                                                                                       |
| ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `reader`    | `fetchMessages`, `fetchChannelMessages`, `fetchThread`, `listThreads`, `getThreadParticipants`, `getChannelInfo`, `getUser`                                                                          |
| `messenger` | `fetchMessages`, `fetchThread`, `getChannelInfo`, `getUser`, `postMessage`, `postChannelMessage`, `sendDirectMessage`, `addReaction`, `removeReaction`, `startTyping`                                |
| `moderator` | All read tools plus `postMessage`, `postChannelMessage`, `sendDirectMessage`, `editMessage`, `deleteMessage`, `addReaction`, `removeReaction`, `subscribeThread`, `unsubscribeThread`, `startTyping` |

## Limiting what the agent can read

Read tools take a thread or channel id chosen by the model, and they run with your bot's platform access. A bot is usually a member of channels a given user is not, so an agent handling untrusted input can be talked into fetching a conversation the user could never open themselves, then repeating it back.

Pass `scope` with the conversation the agent is handling. Reads that resolve outside it are rejected before the platform is called:

```typescript title="lib/bot.ts" lineNumbers
bot.onNewMention(async (thread, message) => {
  const tools = createChatTools({
    chat,
    preset: "reader",
    scope: thread,
  });

  const result = await generateText({
    model: "openai/gpt-5",
    tools,
    prompt: message.text,
  });
});
```

`scope` accepts a `Thread`, a `Channel`, or a raw id. By default scoping is **channel-level**: a read is allowed when it resolves to the same channel as the scoped conversation. That blocks the cross-channel read above, and — matching a channel-membership model — still lets the agent read sibling threads within the channel.

Tools built inside a handler pick this up automatically, so the common case needs no extra argument:

```typescript title="lib/bot.ts" lineNumbers
bot.onNewMention(async (thread, message) => {
  // Reads are confined to this conversation.
  const tools = createChatTools({ chat, preset: "reader" });

  const result = await generateText({
    model: "openai/gpt-5",
    tools,
    prompt: message.text,
  });
});
```

Pass `scope` explicitly when the agent runs outside a handler, such as a queued job that still answers on a user's behalf. Pass a channel id there if the agent legitimately needs to read across the threads in a channel.

<Callout type="warn">
  Agents created outside a handler have no conversation to inherit, so their reads are unrestricted and a warning is logged. Pass `scope` there, or `scope: false` to say workspace-wide reads are intended.
</Callout>

### What `scope` does not do

`scope` confines reads to a conversation using your bot's own access; it is **not** a per-user access check. It does not verify that the end user the agent is answering for is themselves a member of the target — that would require per-platform membership calls the SDK does not make.

So within the scope you grant, reads follow the bot's access, not the user's:

* A **thread** scope still allows reading the parent channel and its sibling threads, matching Slack's channel-membership model (a user who can see a thread can see the channel around it).
* A **channel** scope allows any thread in that channel, including independently permissioned threads on Discord or GitHub that the end user may not have access to.

This channel-level default is coarser than the per-thread ACLs on platforms like Discord and GitHub, where one channel or repo holds independently permissioned threads. `scope` alone will not enforce per-user permissions there.

### Tightening to a single thread

Set `strictScope` to confine a **thread** scope to that thread alone. Sibling threads and the parent channel are both rejected. Recommended when the agent handles untrusted input on a per-thread-ACL platform:

```typescript title="lib/bot.ts" lineNumbers
bot.onNewMention(async (thread, message) => {
  const tools = createChatTools({
    chat,
    preset: "reader",
    scope: thread,
    strictScope: true, // sibling threads and the parent channel are rejected
  });

  const result = await generateText({
    model: "openai/gpt-5",
    tools,
    prompt: message.text,
  });
});
```

Rejecting the parent channel matters most on the platforms this is aimed at, because a channel there is the widest read available. A GitHub channel is an entire repository, so a channel-level read would list every issue and pull request in it: broader than any sibling thread the option blocks.

A **channel** scope is unaffected by `strictScope`. It still allows any thread within the channel, and the channel itself. This is opt-in rather than the default so existing channel-membership-style behavior is preserved; enable it when you want reads confined to the exact thread.

## Approval control

Write operations (posting, editing, deleting, reacting, subscribing) default to `needsApproval: true`. The AI SDK pauses execution and surfaces an approval request that your application is expected to confirm before the tool runs. This keeps a human in the loop for anything visible to the workspace.

```typescript
// All writes need approval (default)
createChatTools({ chat });

// No approval needed
createChatTools({ chat, requireApproval: false });

// Per-tool — only destructive actions need approval
createChatTools({
  chat,
  requireApproval: {
    deleteMessage: true,
    editMessage: true,
    sendDirectMessage: false,
    postMessage: false,
    addReaction: false,
  },
});
```

Read tools (`fetchMessages`, `fetchThread`, `getChannelInfo`, …) and the `startTyping` indicator never require approval.

<Callout>
  `createChatTools` only marks a tool as needing approval — capturing the
  decision, and (optionally) signing it so a resumed approval can't be forged,
  is handled by the AI SDK itself. If you round-trip approvals through an
  untrusted client and enable the AI SDK's `experimental_toolApprovalSecret`,
  use a version of `ai` that includes the injective-payload signing fix
  ([vercel/ai#17493](https://github.com/vercel/ai/pull/17493)).
</Callout>

## Cherry-picking tools

Each tool is also exported as a standalone factory you can hand to `tools` directly:

```typescript title="lib/agent.ts" lineNumbers
import { fetchMessages, postMessage, addReaction } from "chat/ai";

const tools = {
  fetchMessages: fetchMessages(chat),
  postMessage: postMessage(chat, { needsApproval: false }),
  addReaction: addReaction(chat, { needsApproval: false }),
};
```

Useful when you want a small, targeted toolset without going through `createChatTools`.

## Tool overrides

Customize any AI SDK [`tool()`](https://ai-sdk.dev/docs/ai-sdk-core/tools-and-tool-calling) property per tool, keyed by tool name:

```typescript
import type { ChatToolName, ToolOverrides } from "chat/ai";

createChatTools({
  chat,
  overrides: {
    postMessage: {
      description: "Reply in the active customer support thread.",
      needsApproval: false,
    },
    deleteMessage: { needsApproval: true },
  },
});
```

| Property           | Type                  | Description                                                   |
| ------------------ | --------------------- | ------------------------------------------------------------- |
| `description`      | `string`              | Custom tool description shown to the model                    |
| `title`            | `string`              | Human-readable title                                          |
| `strict`           | `boolean`             | Strict mode for input generation                              |
| `inputExamples`    | `array`               | Examples that show the model what tool input should look like |
| `metadata`         | `object`              | Tool metadata propagated to tool call and result parts        |
| `needsApproval`    | `boolean \| function` | Gate execution behind an approval request                     |
| `providerOptions`  | `ProviderOptions`     | Provider-specific metadata                                    |
| `onInputStart`     | `function`            | Callback when argument streaming starts                       |
| `onInputDelta`     | `function`            | Callback on each streaming delta                              |
| `onInputAvailable` | `function`            | Callback when full input is available                         |
| `toModelOutput`    | `function`            | Custom mapping of tool result to model output                 |

Core properties (`execute`, `inputSchema`, `outputSchema`, and tool-kind fields like `type`, `id`, `args`) cannot be overridden so tool semantics stay stable.

## Available tools

All ids accept the full Chat SDK form: `slack:C123ABC:1234567890.123456` for a thread, `slack:C123ABC` for a channel, and the platform-native user id (e.g. `U123456` on Slack, `users/123` on Google Chat). The tools auto-detect the adapter from the prefix.

### Reading

| Tool                    | Description                                                     |
| ----------------------- | --------------------------------------------------------------- |
| `fetchMessages`         | Fetch recent messages from a thread (paginated)                 |
| `fetchChannelMessages`  | Fetch top-level messages in a channel (not thread replies)      |
| `fetchThread`           | Fetch metadata for a thread (channel id, visibility, DM status) |
| `listThreads`           | List recent threads in a channel with their root message        |
| `getThreadParticipants` | Return the unique non-bot participants in a thread              |
| `getChannelInfo`        | Fetch channel metadata (name, member count, visibility)         |
| `getUser`               | Look up a user's profile by id                                  |

### Writing

| Tool                 | Description                                          | Default approval |
| -------------------- | ---------------------------------------------------- | ---------------- |
| `postMessage`        | Post a reply in an existing thread                   | required         |
| `postChannelMessage` | Post a top-level message in a channel                | required         |
| `sendDirectMessage`  | Open a DM with a user and post in it                 | required         |
| `editMessage`        | Edit a message the bot previously posted             | required         |
| `deleteMessage`      | Delete a message the bot previously posted           | required         |
| `addReaction`        | Add an emoji reaction to a message                   | required         |
| `removeReaction`     | Remove a previously-added reaction                   | required         |
| `subscribeThread`    | Subscribe the bot to all future messages in a thread | required         |
| `unsubscribeThread`  | Stop receiving non-mention messages in a thread      | required         |
| `startTyping`        | Show a typing indicator in a thread                  | not gated        |

## API

### `createChatTools(options)`

Returns an object of tools, ready to spread into `tools` of any AI SDK call.

```typescript
type ChatToolsOptions = {
  chat: Chat;
  requireApproval?: boolean | Partial<Record<ChatWriteToolName, boolean>>;
  preset?: ChatToolPreset | ChatToolPreset[];
  overrides?: Partial<Record<ChatToolName, ToolOverrides>>;
  scope?: ReadScope | false;
  strictScope?: boolean;
};

type ChatToolPreset = "reader" | "messenger" | "moderator";
type ReadScope = string | { id: string };
```

| Option            | Description                                                                                                                                                                                                  |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `chat`            | The `Chat` instance the tools dispatch operations against. Required.                                                                                                                                         |
| `preset`          | Preset (or array of presets) restricting which tools are returned. Omit to get every tool.                                                                                                                   |
| `requireApproval` | `true` (default), `false`, or per-tool overrides. Read tools and `startTyping` are never gated.                                                                                                              |
| `overrides`       | Per-tool customization of any AI SDK `tool()` property except `execute`, `inputSchema`, and `outputSchema`.                                                                                                  |
| `scope`           | Conversation the read tools are confined to. Defaults to the conversation being handled. Pass a `Thread`, `Channel`, or `false` to read across every conversation the bot can see. Channel-level by default. |
| `strictScope`     | `false` (default). Set `true` to tighten a thread `scope` to that thread alone, rejecting both sibling threads and the parent channel. A channel scope is unaffected.                                        |


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)