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_searchStep 4 — Type-check and run
bash
npm run typecheck # Verify no TypeScript errors
npm run dev # Gateway auto-discovers and registers the new toolBest 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 logger —
createLogger('tool-name')gives structured, filterable output - Respect the abort signal — for long-running operations, check
ctx.signal.abortedperiodically 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.