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
- You define a
toolsarray with name, description, and JSON schema for the parameters. - Pass it to
messages.createalong with the conversation. - Claude may decide to call a tool. Instead of returning
text, it returns atool_useblock with arguments. - You run the function on your server.
- Send the result back as a message with
role: "user"andcontentof typetool_result. - 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, notsearch. - Enum parameters when possible.
sort: "price_asc" | "price_desc"beatssort: string. - Return structured JSON, not plain text. The model digests it better.
- No secrets in
descriptionor 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.
Pro challenge
Pro challenge for this lesson
Same idea, no hints, graded by automated tests.
Hard Mode
Hard Mode
Extreme variant of the challenge. Time-bound with extra constraints.