Skip to content

Lesson 03

Tool use: give your chatbot superpowers

Your bot stops hallucinating and starts calling real APIs. Tool calling with Claude — full loop pattern.


The line between demo and product

Without tool use, the bot makes things up. Ask it the weather in Madrid and it will tell you (poorly).

With tool use, the bot looks. Ask the weather and it calls your weather API, receives real JSON, and tells you.

How it works in Claude

  1. You define a tools array with name, description, and JSON schema for the parameters.
  2. Pass it to messages.create along with the conversation.
  3. Claude may decide to call a tool. Instead of returning text, it returns a tool_use block with arguments.
  4. You run the function on your server.
  5. Send the result back as a message with role: "user" and content of type tool_result.
  6. Claude reads the result and replies to the user in natural language.

It is a loop. You keep going until Claude returns stop_reason: "end_turn".

Example: weather tool

1. Define the tool

const tools: Anthropic.Tool[] = [
  {
    name: "get_weather",
    description: "Gets the current weather for a city.",
    input_schema: {
      type: "object",
      properties: {
        city: { type: "string", description: "City name, e.g. 'Madrid'." },
      },
      required: ["city"],
    },
  },
];

2. The implementation

async function getWeather(city: string) {
  const res = await fetch(
    `https://wttr.in/${encodeURIComponent(city)}?format=j1`,
  );
  const data = await res.json();
  const cur = data.current_condition[0];
  return {
    city,
    temperature: cur.temp_C + "°C",
    description: cur.weatherDesc[0].value,
    humidity: cur.humidity + "%",
  };
}

3. The loop

async function chat(userMessages: Anthropic.MessageParam[]) {
  const messages = [...userMessages];

  while (true) {
    const res = await client.messages.create({
      model: "claude-opus-4-7",
      max_tokens: 1024,
      tools,
      messages,
    });

    if (res.stop_reason === "end_turn") {
      const text = res.content.find((c) => c.type === "text");
      return text?.type === "text" ? text.text : "";
    }

    if (res.stop_reason === "tool_use") {
      messages.push({ role: "assistant", content: res.content });

      const results: Anthropic.ToolResultBlockParam[] = [];
      for (const block of res.content) {
        if (block.type !== "tool_use") continue;

        if (block.name === "get_weather") {
          const input = block.input as { city: string };
          const data = await getWeather(input.city);
          results.push({
            type: "tool_result",
            tool_use_id: block.id,
            content: JSON.stringify(data),
          });
        }
      }

      messages.push({ role: "user", content: results });
      continue;
    }

    return "";
  }
}

Try it

const reply = await chat([
  { role: "user", content: "What's the weather in Buenos Aires and Madrid right now?" },
]);
console.log(reply);
// → "Buenos Aires is 14°C and clear. Madrid is 22°C and sunny."

Claude calls get_weather twice (one per city) in parallel, receives the results, composes the reply.

Tool design — rules you learn the hard way

  • Honest descriptions: if your tool sometimes fails, say so. The model will decide whether to call it.
  • Explicit names: search_products, not search.
  • Enum parameters when possible. sort: "price_asc" | "price_desc" beats sort: string.
  • Return structured JSON, not plain text. The model digests it better.
  • No secrets in description or in the schema. It goes to the model every turn.

Up next

Your bot already remembers and can query the real world. Next lesson we wrap it in a ChatGPT-like UI with streaming.


LEVEL 2

Pro challenge

Pro challenge for this lesson

Same idea, no hints, graded by automated tests.

Unlock Pro Mode · €29 One-time payment · lifetime access · no subscription
LEVEL 3

Hard Mode

Hard Mode

Extreme variant of the challenge. Time-bound with extra constraints.

Unlock Pro Mode · €29 One-time payment · lifetime access · no subscription