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.1s
TipWrite 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