Tools

Creating a Custom Tool

This guide walks through creating a complete custom tool from scratch. We will build a send_slack_message tool that lets agents post messages to a Slack channel.

Step 1 — Create the tool file

Create src/tools/send-slack-message.ts:

typescript
// src/tools/send-slack-message.ts
import { z } from 'zod';
import { registerTool } from './registry';
import { createLogger } from '../utils/logger';

const logger = createLogger('send-slack-message');

// Define the parameter schema with Zod
const Params = z.object({
  channel: z.string().describe('Slack channel ID or name (e.g. #general or C01234567)'),
  message: z.string().describe('The message text to send (markdown supported)'),
  threadTs: z.string().optional().describe('Thread timestamp to reply to a thread'),
  blocks: z.array(z.unknown()).optional().describe('Slack Block Kit blocks for rich formatting'),
});

// Register the tool at module scope — runs when this file is imported
registerTool({
  name: 'send_slack_message',
  description: 'Send a message to a Slack channel or thread',
  parameters: Params,
  execute: async (params, ctx) => {
    const token = process.env.SLACK_BOT_TOKEN;
    if (!token) {
      return { error: 'SLACK_BOT_TOKEN not configured' };
    }

    const body: Record<string, unknown> = {
      channel: params.channel,
      text: params.message,
    };
    if (params.threadTs) body.thread_ts = params.threadTs;
    if (params.blocks) body.blocks = params.blocks;

    const response = await fetch('https://slack.com/api/chat.postMessage', {
      method: 'POST',
      headers: {
        'Authorization': \`Bearer ${token}\`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(body),
    });

    const data = await response.json();

    if (!data.ok) {
      logger.warn('Slack API error', { error: data.error, agentId: ctx.agentId });
      return { error: \`Slack error: ${data.error}\` };
    }

    logger.info('Message sent', { channel: params.channel, agentId: ctx.agentId });
    return {
      messageTs: data.ts,
      channel: data.channel,
      permalink: \`https://slack.com/archives/${data.channel}/p${data.ts.replace('.', '')}\`,
    };
  },
});

Step 2 — Register the import

Add the import to src/tools/index.ts:

typescript
// src/tools/index.ts
// ... existing imports ...
import './send-slack-message';

Step 3 — Allow the tool in an agent

yaml
agents:
  - id: notifier-agent
    tools:
      allow:
        - send_slack_message
        - web_search

Step 4 — Type-check and run

bash
npm run typecheck    # Verify no TypeScript errors
npm run dev          # Gateway auto-discovers and registers the new tool

Best practices

  • Always return errors as strings, not thrown exceptions. The agent loop handles error strings gracefully; thrown exceptions crash the tool round.
  • Validate prerequisites early — check for required env vars or config at the top of execute() and return a clear error if missing
  • Use the loggercreateLogger('tool-name') gives structured, filterable output
  • Respect the abort signal — for long-running operations, check ctx.signal.aborted periodically and cancel if true
  • Keep descriptions accurate — the description field is sent to the model to explain when to use the tool. Be specific about what it does and does not do.
Tool names must be unique across the entire registry. Use a prefix (e.g. your company or service name) if you are building tools that will be shared across multiple projects.