AutoGen Tutorial (TypeScript): building custom tools for beginners

By Cyprian AaronsUpdated 2026-04-21
autogenbuilding-custom-tools-for-beginnerstypescript

This tutorial shows you how to build a custom AutoGen tool in TypeScript, register it with an agent, and let the agent call it safely. You need this when you want your agent to do something deterministic like fetching policy data, validating input, or querying an internal service instead of hallucinating an answer.

What You'll Need

  • Node.js 18+
  • A TypeScript project with ts-node or tsx
  • autogen-ext
  • @modelcontextprotocol/sdk
  • zod
  • An OpenAI API key set as OPENAI_API_KEY
  • Basic familiarity with AutoGen agents and async/await

Install the packages:

npm install autogen-ext @modelcontextprotocol/sdk zod
npm install -D typescript tsx @types/node

Step-by-Step

  1. Start by defining a tool as a typed function. For beginners, keep the first tool simple: accept a customer name and return a deterministic lookup result.
import { z } from "zod";
import { FunctionTool } from "autogen-ext/tools";

const customerLookupSchema = z.object({
  customerId: z.string().min(1),
});

const lookupCustomer = async ({ customerId }: { customerId: string }) => {
  const customers: Record<string, { name: string; tier: string }> = {
    "1001": { name: "Amina Patel", tier: "Gold" },
    "1002": { name: "Jordan Lee", tier: "Silver" },
  };

  return customers[customerId] ?? { name: "Unknown", tier: "Not found" };
};

export const lookupCustomerTool = new FunctionTool(lookupCustomer, {
  name: "lookup_customer",
  description: "Look up a customer by ID.",
  schema: customerLookupSchema,
});
  1. Next, create an assistant agent and give it the tool. The important part is that the model can decide when to call the tool, but the tool itself stays under your control.
import { AssistantAgent } from "autogen-ext/agents";
import { OpenAIChatCompletionClient } from "autogen-ext/models/openai";

const modelClient = new OpenAIChatCompletionClient({
  model: "gpt-4o-mini",
  apiKey: process.env.OPENAI_API_KEY,
});

const agent = new AssistantAgent({
  name: "support_agent",
  modelClient,
  tools: [lookupCustomerTool],
});
  1. Now send a prompt that forces a tool call. In production, this is where you would ask for something that needs exact data rather than free-form reasoning.
async function main() {
  const result = await agent.run(
    [
      {
        role: "user",
        content: "Look up customer 1001 and tell me their name and tier.",
      },
    ],
    {
      maxTurns: 3,
    }
  );

  console.log(result.messages);
}

main().catch(console.error);
  1. If you want a more realistic beginner pattern, wrap your tool around an HTTP call. This is how most internal tools work in banking and insurance systems: validate input first, then hit a trusted backend.
import { FunctionTool } from "autogen-ext/tools";
import { z } from "zod";

const policySchema = z.object({
  policyNumber: z.string().min(5),
});

async function getPolicyStatus({ policyNumber }: { policyNumber: string }) {
  const response = await fetch(`https://api.example.com/policies/${policyNumber}`, {
    headers: {
      Authorization: `Bearer ${process.env.POLICY_API_TOKEN}`,
    },
  });

  if (!response.ok) {
    return { status: "error", message: `HTTP ${response.status}` };
  }

  return response.json();
}

export const policyStatusTool = new FunctionTool(getPolicyStatus, {
  name: "get_policy_status",
  description: "Fetch the current status of a policy.",
  schema: policySchema,
});
  1. Finally, add guardrails around the tool output. Beginners often skip this part, but you should return stable JSON-shaped data so the agent can reason over it reliably.
type PolicyResult =
  | { status: "active"; premiumDueDate: string }
  | { status: "lapsed"; lapseDate: string }
  | { status: "error"; message: string };

async function safeGetPolicyStatus({ policyNumber }: { policyNumber: string }): Promise<PolicyResult> {
  try {
    const response = await fetch(`https://api.example.com/policies/${policyNumber}`);
    if (!response.ok) return { status: "error", message: `HTTP ${response.status}` };

    const data = await response.json();
    if (data.status === "active") return { statusCodeSafe(data) };
    if (data.status === "lapsed") return { statusCodeSafe(data) };

    return { statusCodeSafe({ statusMessageOnlyError }) };
  } catch (error) {
    return {
      statusMessageOnlyError:
        error instanceof Error ? error.message : String(error),
    } as PolicyResult;
  }
}

Testing It

Run the script with your API key set in the environment:

OPENAI_API_KEY=your_key_here npx tsx src/main.ts

You should see a tool call in the returned messages, followed by the assistant’s final answer based on the tool output. If the model answers without calling the tool, make the user prompt more specific or lower ambiguity in your tool description.

For debugging, log both the raw messages and any tool results before they go back to the model. That makes it obvious whether failures come from schema validation, network errors, or model behavior.

Next Steps

  • Add multiple tools and compare how AutoGen chooses between them
  • Replace mock data with real internal APIs behind auth and retries
  • Add structured logging so every tool call is traceable in production

Keep learning

By Cyprian Aarons, AI Consultant at Topiax.

Want the complete 8-step roadmap?

Grab the free AI Agent Starter Kit — architecture templates, compliance checklists, and a 7-email deep-dive course.

Get the Starter Kit

Related Guides