# Add tools

> Define typed agent tools with defineChannelTool and any Standard Schema validator (Zod, Valibot, ArkType): the ChannelToolContext shape ({ thread, message?, user, actor, signal?, platform }), return-value rules (raw data back to the agent, error text on failure), and registration via createChannel({ tools }) or channel.tool().

- Repository: CopilotKit/channels-sdk
- GitHub: https://github.com/CopilotKit/channels-sdk
- Human docs: https://grok-wiki.com/public/docs/copilotkit-channels-sdk-4c947e3e6161
- Complete Markdown: https://grok-wiki.com/public/docs/copilotkit-channels-sdk-4c947e3e6161/llms-full.txt

## Source Files

- `.agents/skills/build-channels-agent/SKILL.md`
- `.agents/skills/build-channels-agent/evals/evals.json`
- `.agents/skills/build-channels-agent/references/hitl-patterns.md`
- `README.md`

---

---
title: "Add tools"
description: "Define typed agent tools with defineChannelTool and any Standard Schema validator (Zod, Valibot, ArkType): the ChannelToolContext shape ({ thread, message?, user, actor, signal?, platform }), return-value rules (raw data back to the agent, error text on failure), and registration via createChannel({ tools }) or channel.tool()."
---

`defineChannelTool` from `@copilotkit/channels` declares a typed function the agent can call during a run. A tool is a plain object with a `name`, a `description`, a `parameters` schema, and an async `handler`. The `parameters` field accepts any [Standard Schema](https://standardschema.dev) validator — Zod, Valibot, and ArkType all work — and the handler receives the parsed, typed arguments plus a `ChannelToolContext` that carries the **live thread**, so a tool can post UI, ask a question, or run a human-in-the-loop flow mid-execution.

<Warning>
The API is `defineChannelTool`, not `defineBotTool`. Bot-prefixed names (`createBot`, `defineBotTool`) do not exist in this SDK and are a documented hallucination pattern — see the troubleshooting page.
</Warning>

## Define a tool

```ts channel.ts
import { defineChannelTool } from "@copilotkit/channels";
import { z } from "zod";

const getOncall = defineChannelTool({
  name: "get_oncall",
  description: "Look up who is currently on call for a team.",
  parameters: z.object({ team: z.string() }),
  async handler({ team }, { thread, user, actor, signal, platform }) {
    return await fetchOncall(team); // returned value goes back to the agent
  },
});
```

<ParamField body="name" type="string" required>
Tool name the model calls, e.g. `get_oncall`.
</ParamField>

<ParamField body="description" type="string" required>
What the tool does — this is the model's only guidance for when to call it.
</ParamField>

<ParamField body="parameters" type="StandardSchema" required>
Any Standard Schema validator (Zod, Valibot, ArkType). The handler receives the parsed output type; invalid arguments never reach your code.
</ParamField>

<ParamField body="handler" type="(args, ctx: ChannelToolContext) => Promise<unknown>" required>
Async function receiving the validated arguments and the tool context. Its return value is serialized back to the agent.
</ParamField>

## ChannelToolContext

The second handler argument is `ChannelToolContext = { thread, message?, user, actor, signal?, platform }`:

<ResponseField name="thread" type="Thread">
The live per-conversation handle. Everything on the Thread API is available mid-tool-call: `post`, `update`, `awaitChoice`, `postFile`, `subscribe`, and the rest.
</ResponseField>

<ResponseField name="message" type="Message | undefined">
The triggering message, when the run originated from one. Optional — not every run starts from a message.
</ResponseField>

<ResponseField name="user" type="ApplicationUser | null">
The canonical user resolved by `identifyUser`. Note that the `onMention`/`onMessage` handlers receive only `{ thread, message }`; the tool context is one of the places (alongside `onThreadStarted` and `onWelcome`) where the resolved `user` is exposed directly.
</ResponseField>

<ResponseField name="actor" type="Actor">
The platform-level actor behind the turn.
</ResponseField>

<ResponseField name="signal" type="AbortSignal | undefined">
Cancellation signal for the run. Pass it to long-running fetches so an aborted turn stops your work.
</ResponseField>

<ResponseField name="platform" type="string">
The surface the turn is running on (e.g. Slack, Teams), for platform-conditional behavior.
</ResponseField>

## Return-value rules

The return value is what the **agent** reads back — the user never sees it directly.

| Situation | Return |
| --- | --- |
| Data lookup | The raw data. It is JSON-stringified for you — do not hand-stringify, and do not return `{ ok: true }`. |
| Tool posted UI itself | A short natural-language confirmation such as `"Displayed the issue card."` so the model doesn't restate the card's content in prose. |
| Failure | The actual error text, so the model can repair its arguments and retry. Do not throw away the message or return a bare boolean. |

## Register tools

<Tabs>
<Tab title="createChannel({ tools })">

```ts
import { createChannel } from "@copilotkit/channels";

const channel = createChannel({
  name: process.env.CHANNEL_CODE!,
  identifyUser: "platform",
  agent: makeAgent,
  tools: [getOncall],
});
```

</Tab>
<Tab title="channel.tool()">

```ts
// Must happen before the runtime starts the Channel
channel.tool(getOncall);
```

Registration via `channel.tool(t)` is equivalent, but it has to run before the listener is created — creating the listener is what starts the Channel.

</Tab>
<Tab title="Per-run via runAgent">

```ts
await thread.runAgent({
  prompt,
  tools: [oneOffTool], // extra ChannelTool[] for this run only
});
```

`thread.runAgent({ tools })` adds tools for a single run on top of the Channel-level registration.

</Tab>
</Tabs>

On the direct Slack adapter path, spread in the SDK's defaults alongside your own — `defaultSlackTools` adds `lookup_slack_user` plus tagging, mrkdwn, and threading guidance:

```ts
import { slack, defaultSlackTools, defaultSlackContext } from "@copilotkit/channels/slack";

const channel = createChannel({
  name: "support-slack",
  identifyUser: "platform",
  adapters: [slack({ botToken, appToken })],
  agent: makeAgent,
  tools: [...defaultSlackTools, getOncall],
  context: [...defaultSlackContext],
});
```

<Note>
Tools and context solve different problems. A `ContextEntry` (`{ description, value }`) is injected into the agent's prompt every run — use it for standing facts like the channel or the caller's role. A tool is a function the model chooses to call. Register context via `createChannel({ context })` or `thread.runAgent({ context })`.
</Note>

## Gate a tool on human approval

Because the handler holds the live `thread`, it can block on a typed button choice before doing anything irreversible. `thread.awaitChoice<T>` posts the UI and suspends the handler until a control is activated, resolving to that control's `value`:

```tsx delete-tool.tsx
import { defineChannelTool, Message, Section, Markdown, Actions, Button } from "@copilotkit/channels";
import { z } from "zod";

const dropDatabase = defineChannelTool({
  name: "drop_database",
  description: "Delete a database after human confirmation.",
  parameters: z.object({ db: z.string() }),
  async handler({ db }, { thread }) {
    const ok = await thread.awaitChoice<boolean>(
      <Message accent="#E01E5A">
        <Section><Markdown>Delete **{db}**? This is irreversible.</Markdown></Section>
        <Actions>
          <Button value={true} style="primary">Approve</Button>
          <Button value={false} style="danger">Cancel</Button>
        </Actions>
      </Message>,
    );
    if (!ok) return "User cancelled; nothing was deleted.";
    await deleteDatabase(db);
    return `Deleted ${db}.`;
  },
});
```

Files containing JSX must be `.tsx`, and the tsconfig must set `jsxImportSource: "@copilotkit/channels"` — this JSX runtime is not React and declares no lowercase intrinsic tags (`<b>`, `<span>` are compile errors; emphasis goes inside `<Markdown>`).

## Tool-call progress in the conversation

Managed Slack hides tool-call progress by default; the conversation shows only the clean result, while lifecycle events still land in Intelligence history. Opt in per Channel:

```ts
const channel = createChannel({ /* … */ showToolStatus: true });
```

`showToolStatus` is ignored for direct-adapter Channels — configure those on the adapter instead, e.g. `slack({ showToolStatus: true })`.

## Components as agent-callable tools

From 0.7+, `defineChannelComponent` turns a JSX component into a tool the agent can call to render UI itself, with props inferred from the same Standard Schema mechanism. Pass those via `createChannel({ components })`, not `tools` — registration also lets keyed handlers be recovered after a restart when the store is durable.

## Related pages

<CardGroup cols={2}>
  <Card title="Human-in-the-loop approvals" href="/human-in-the-loop">
    `awaitChoice` in depth, `onInterrupt` + `thread.resume`, and making approval buttons survive restarts.
  </Card>
  <Card title="createChannel reference" href="/createchannel-reference">
    Every `createChannel` option, including `tools`, `context`, `components`, and `showToolStatus`.
  </Card>
  <Card title="Thread API reference" href="/thread-api-reference">
    The full `thread` surface available inside a tool handler, including capability-gated methods.
  </Card>
  <Card title="Render interactive UI" href="/render-interactive-ui">
    The JSX vocabulary tools can post, and `defineChannelComponent` for agent-rendered UI.
  </Card>
  <Card title="Handle mentions, messages, and subscriptions" href="/handle-conversations">
    The channel handlers that trigger the runs where your tools execute.
  </Card>
  <Card title="Troubleshooting" href="/troubleshooting">
    Known failure modes, including the invented Bot-prefixed APIs and JSX-against-React compile errors.
  </Card>
</CardGroup>
