Writing Custom Tools
Extend the agent with your own TypeScript functions. Custom tools work exactly like built-in tools — the model can call them, their output is logged, and they respect the same timeout and retry settings.
Anatomy of a tool
Every tool needs four things:
- name — a snake_case identifier the model uses to call it
- description — plain English explaining what it does and when to use it
- input — a Zod schema defining the arguments
- execute — an async function that receives the validated input and returns a result
Full example
tools/send-slack.ts
import { defineTool } from "testy-claw";
import { z } from "zod";
export const sendSlack = defineTool({
name: "send_slack_message",
description:
"Send a message to a Slack channel. Use this when the task requires " +
"notifying a team or posting a status update.",
input: z.object({
channel: z.string().describe("Slack channel name, e.g. #deployments"),
message: z.string().describe("The message text to send"),
}),
execute: async ({ channel, message }) => {
const res = await fetch("https://slack.com/api/chat.postMessage", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.SLACK_BOT_TOKEN}`,
},
body: JSON.stringify({ channel, text: message }),
});
const data = await res.json();
if (!data.ok) throw new Error(`Slack error: ${data.error}`);
return { ok: true, ts: data.ts };
},
});Register in config
claw.config.ts
import { defineConfig } from "testy-claw";
import { sendSlack } from "./tools/send-slack";
export default defineConfig({
tools: ["fs", "shell", sendSlack],
});Use in a task
terminal
testy-claw run "deploy the app and notify #deployments on Slack when done"
✓ shell.exec("npm run build")
✓ shell.exec("npm run deploy")
✓ send_slack_message({ channel: "#deployments", message: "Deploy complete ✓" })
✓ Done in 22.1sTipWrite clear, specific
description strings. The model uses them to decide when to call your tool — vague descriptions lead to missed or incorrect calls.Best practices
- Keep tools focused — one tool, one responsibility
- Return structured data (objects) rather than plain strings when possible
- Throw descriptive errors — the model will see them and can retry
- Use Zod .describe() on each field to give the model better context
- Store secrets in environment variables, never hardcode them