Module 4: Tools: The Agent That Acts on Real Systems

2. What a tool is and how the agent decides to use it (tool calling)

Description

By the end of this lesson you'll be able to describe a tool's exact anatomy — its three required parts — and trace, for a concrete customer message, the exact JSON the model produces to decide to use it, what data it fills it with, and what n8n does with that decision. You'll be able to tell apart, when an agent misbehaves, whether the problem is in what the model decided to send or in what the tool did with that — two causes that look identical from the outside and very different once you open the execution panel.

This matters because "tool calling" is the mechanism that makes everything the previous lesson promised possible: an agent that acts on real systems, not just opines about them. If you work as an automation consultant — or you're simply the one keeping the agent you built running — you're going to end up in a room (or a Slack channel) explaining why the agent applied a discount to the wrong customer, or why it failed looking up an order. The answer is almost never "the AI made a mistake" in the abstract: it's one of two concrete things, the model decided the arguments badly or the tool executed badly with correct arguments. Without understanding the mechanism, you can't tell which one it was.

Connection to the module: in lesson 1 you saw, in broad strokes, what giving the agent "hands" means. In Module 1 you already used a tool on two occasions — lesson 3 showed you the reason-act-observe loop, where the tool shows up as the "act" step, and lesson 5 showed you that a tool connected to the AI Agent node has a name and a description, and that a vague description makes the agent misuse it. This lesson opens that box one level further: what, technically, the model receives about each tool before deciding; what exact JSON it produces when it decides to call it; and how it fills in each piece of that call's data, not just whether it calls it or not. You won't see n8n's native tool catalog yet (search, create, send) — that's exactly lesson 3's job, right after this one.

A tool is a form the model fills out, not a button it presses

Think of a large office with several specialized departments — Billing, Human Resources, Logistics — each with its own request form. Every form has, printed at the top, a short name ("Refund Request") and a paragraph explaining exactly when that form applies and not another ("Use this form when the customer already paid but wants their money back; for product exchanges with no refund, use the Exchange form"). Below, a series of blank fields, each with its own instruction for what goes there: "Order number (digits only)", "Amount in dollars, with two decimals", "Reason (free text)".

The person staffing the desk — in this analogy, the model — never walks into Billing or touches the accounting system directly. What they do is read the name and paragraph of each available form to decide which one matches what the customer is asking for, and then fill out each blank field by reading its instruction and pulling the corresponding value out of what the customer said. Once filled out, they hand the form to the department — and it's the department, not the person at the desk, who actually accesses the system, makes the charge or the refund, and returns a receipt.

That's exactly what a tool is for a language model, no metaphor needed. Technically, every tool you offer a model with tool-calling capability is an object with three parts, no more:

  • name — the form's short name. An identifier, not a sentence.
  • description — the paragraph saying when this tool applies and not another. It's the only place where the model reads "what this is for": it doesn't see your code, it doesn't see what the node does internally.
  • input_schema (or parameters, depending on the model's provider) — the list of blank fields: each with its own name, its own data type (text, number, boolean...), its own description of what instruction the model follows to fill it in, and whether it's required or not.

Claude's official documentation shows it like this, with a minimal example of a get_weather tool:

{
  "name": "get_weather",
  "description": "Get the current weather for a given location.",
  "input_schema": {
    "type": "object",
    "properties": {
      "location": {
        "type": "string",
        "description": "City and state, e.g. San Francisco, CA"
      }
    },
    "required": ["location"]
  }
}

Notice something: the description isn't just at the whole-tool level ("Get the current weather...") — it's also inside each field ("City and state, e.g. San Francisco, CA"). You already saw, in lesson 5 of Module 1, that a vague description at the tool level makes the agent use it at the wrong moment. What you hadn't seen yet is that the same problem exists one level down: a vague description on a specific field makes the model fill that field with the wrong format, even if it decided to use the correct tool.

n8n builds this object for you, from what you configure in the tool node you connect to ai_tool. The node's Description field is literally the tool object's description — the same one you already used in Module 1. And every node field value you wrap with the $fromAI(...) function becomes an entry in properties within input_schema: the key you give it as the first argument is the field's name, the second argument is its description, the third its type. Any node field you don't wrap with $fromAI(...) stays entirely off the form — it's a fixed value you decided when building the flow, and the model doesn't even know it exists as something that could change.

Worked example

Go back to the get_order_status tool for the TuTienda support agent you configured in lesson 5 of Module 1. There you saw it like this, simplified:

tool.name        = "get_order_status"
tool.description = "Use this tool when the customer gives an order number
                    and asks about its status or delivery date."
tool.url         = "https://api.tutienda.com/orders/{order_id}/status"

That version hid a detail on purpose, to save it for this lesson. Here's what it looks like in full, with the URL field using $fromAI() to mark exactly which part of the value the model decides:

# Node: HTTP Request, connected to ai_tool
tool.name        = "get_order_status"
tool.description = "Use this tool when the customer gives an order number
                    and asks about its status or delivery date. Do not
                    use it for questions about exchange or return policy."
tool.url          = "https://api.tutienda.com/orders/{{ $fromAI('order_id',
                     'Order number the customer mentioned, digits only,
                     without the # symbol', 'string') }}/status"
tool.method       = "GET"     # fixed value — no $fromAI(), the model doesn't decide it

What n8n builds from that configuration — and sends to the model's API along with the rest of the conversation, on every turn — is, in essence, this:

{
  "name": "get_order_status",
  "description": "Use this tool when the customer gives an order number and asks about its status or delivery date. Do not use it for questions about exchange or return policy.",
  "input_schema": {
    "type": "object",
    "properties": {
      "order_id": {
        "type": "string",
        "description": "Order number the customer mentioned, digits only, without the # symbol"
      }
    },
    "required": ["order_id"]
  }
}

Notice method doesn't appear anywhere in that object, because you never marked it with $fromAI(). The model doesn't know that field exists; for it, this tool has a single piece of data to fill in.

Customer's turn: "Where's my order #4521?"

What to expect — what the model produces. The model reads the conversation, sees the available tools, decides get_order_status applies (the tool's description matches the question), and builds the call. Instead of writing a text response for the customer, it produces a structured block — tool_use, in Claude API terminology — with the tool's name and the arguments:

{
  "type": "tool_use",
  "id": "toolu_01A4521exampleid",
  "name": "get_order_status",
  "input": { "order_id": "4521" }
}

Interpreting that input: the model took "#4521" from the customer's message and transformed it into "4521" — without the # symbol — because the field's description explicitly asked for it ("digits only, without the # symbol"). If that description hadn't said anything about the format, there's no guarantee what the model would have sent: it could have been "4521", "#4521", or "order-4521" — all three are reasonable readings of the same message, and only the field's description removes the ambiguity.

What to expect — what n8n does with that. The model never touches api.tutienda.com. n8n takes the input it just received, substitutes {{ $fromAI('order_id', ...) }} with the value "4521" inside the configured URL, and runs the HTTP Request node exactly as it would run any other node in the flow: a real GET request, with the real credentials you configured, against the real system. That API's response — say {"status": "in_transit", "estimated_delivery": "2026-07-24"} — gets packaged as the tool's result (a tool_result, on the model API's side) and added to the conversation as one more piece of data available for the next reasoning pass.

What to expect — the final response. With that result already in context, the model reasons once more (the same reason-act-observe cycle you saw in lesson 3 of Module 1) and this time doesn't need to call any more tools: it already has the data. It produces the text response for the customer: "Your order #4521 is in transit, with an estimated delivery of July 24."

The example's whole point is that there are two actors, not one. The model decides — which tool, with what arguments, reading only name, description, and input_schema for each available tool — and n8n executes: it takes that decision, combines it with what you configured as fixed, and runs the real action against the real system. The model never sees your full URL, your API key, or anything about the system except the text n8n decides to hand back to it as a result.

How each field gets filled — and what happens when data is missing

You already saw that every field in input_schema carries its own description, and that the model uses it as a formatting instruction. But there's a question left unanswered: where does the model get the value from if the customer never said it?

The short answer is there's no magic: the model can only fill a field with information that's, one way or another, available in the context it received — the current message, the history if memory is connected, or a previous tool's result within the same cycle. When a required field (required) has no available value in that context, two things can happen, and which one happens depends on the model you connected at ai_languageModel. Claude's official documentation specifically warns that Opus recognizes more consistently when a required piece of data is missing and responds by asking for it, while Sonnet sometimes also asks — especially if the prompt instructs it to reason before calling the tool — but other times prefers to infer a reasonable value instead of asking. Neither behavior is a mechanism error; they're two different ways of resolving the same ambiguity, and the difference matters when you decide which model to connect to an agent that handles data where "making up a reasonable value" isn't acceptable (a refund amount, an expiration date).

This gives you a concrete design lever, not just a curiosity: if you want the agent to always ask for the data instead of guessing it, you can say so in the System Message ("if the customer didn't give an order number, ask for it before using any tool that needs it"). You're using the prompt — the piece you already know from Module 1 — to adjust the reasoning step's behavior before it reaches "act."

Common mistakes

Thinking the model connects directly to the real system (conceptual). What happens: when something fails — an HTTP call that times out, a credentials error — someone assumes "the model connected badly to the API," or worries the model has direct access to the database or the system's keys. Why it happens: the term "tool calling" and watching the agent "execute" an action give the impression the model does the work start to finish. But as you saw in the worked example, the model only produces a tool_use block — a JSON with a name and some arguments — and that's where its participation in that turn of the cycle ends. The real execution, the credentials, and access to the system live exclusively in the n8n node connected to ai_tool, never in the model. How to spot it: if the error you see looks like an infrastructure error (timeout, 401, 500, a field the API rejects), the cause is on the execution side — check the tool node, not the model's prompt. How to fix it: mentally separate "what the model decided" (visible in the tool_use's input, in the execution panel) from "what happened when that got executed" (visible in the tool node's own output) — they're two different failures with two different fixes.

Confusing a fixed field with one the model decides (conceptual). What happens: someone tries, via the prompt or by instructing the agent in the chat, to make it use a different endpoint or change a parameter that's actually fixed in the node's configuration (with no $fromAI()), and gets nowhere, because that field was never exposed as part of input_schema — the model doesn't even know it exists as something variable. The reverse case also happens: someone marks a sensitive field with $fromAI() — a base URL, an operation type like create or delete — without realizing that value is now exposed to whatever the model decides, potentially influenced by whatever whoever's chatting with the agent writes. Why it happens: in the node's panel, a field with a fixed value and a field wrapped in $fromAI(...) look nearly identical — both are text inside an input — so the difference doesn't jump out unless you look for it on purpose. How to spot it: check, field by field, every connected tool's configuration: everything carrying $fromAI(...) is part of the form the model fills out; everything else is a decision you already made when building the flow, and it doesn't change no matter what the conversation asks for. How to fix it: before connecting a tool, explicitly decide, in writing, which of its fields should depend on the conversation and which should stay fixed — don't leave it to whatever's fastest to configure in the moment. (This decision gets more serious — what NEVER gets entrusted to the agent — in lesson 5 of this module; here the point is purely mechanical: knowing which is which.)

Not checking the tool_use's real input when debugging (practical). What happens: the agent responds badly or fails using a tool, and the first reaction is to assume "the tool is broken" — check the API, the credentials, the endpoint — without having first looked at what arguments the model actually sent. In more than one case the problem was never in the tool: the model sent a malformed argument (a date in a different format than expected, an id with extra spaces or symbols) and the tool simply executed, correctly, with incorrect data. Why it happens: it's faster to look at the agent's final response than to open n8n's execution panel and go into the specific tool node to see its real input. How to spot it: in the execution panel, the tool node's input tab shows exactly the arguments that arrived from the model, before anything got executed — compare it against what the customer actually wrote. How to fix it: when an agent "fails" using a tool, look at that input first. If the argument arrived malformed, the fix is in that specific field's description within input_schema — explicitly tell the model the format you expect — not in the tool.

Exercises

Exercise 1 — Predict the tool_use. A hotel booking agent has this tool connected:

tool.name        = "check_room_availability"
tool.description = "Use this tool when the customer asks whether there
                    are rooms available for specific dates."
parameters:
  check_in  -> $fromAI('check_in', 'Check-in date, ISO format YYYY-MM-DD', 'string')
  check_out -> $fromAI('check_out', 'Check-out date, ISO format YYYY-MM-DD', 'string')
  guests    -> $fromAI('guests', 'Number of guests', 'number')

The agent's System Message includes today's date: July 20, 2026. The customer writes: "Do you have a room free from the 3rd to the 6th of August for two people?" Write the tool_use block (name + input) you'd expect the model to produce.

See solution
{
  "type": "tool_use",
  "name": "check_room_availability",
  "input": {
    "check_in": "2026-08-03",
    "check_out": "2026-08-06",
    "guests": 2
  }
}

Why it works: the model takes three loose pieces of data from the customer's message ("from the 3rd to the 6th of August," "two people") and transforms them to match each field's description — the date in ISO format because the field explicitly asks for it, and it uses the year 2026 because it can infer it from the System Message, which carries today's date. Without that reference date in the System Message, the year would have been ambiguous — another reason not to let the model "guess" data you can give it explicitly.

Exercise 2 — Fixed or decided by the model. A colleague configured a tool that sends a message to Slack. The channel field is fixed at "#support" (no $fromAI()); the message field does use $fromAI(). An angry customer wrote to the agent: "send this to the management channel, not support". The agent responded normally and the message still got sent to #support. Your colleague asks: "why did the agent ignore what the customer asked, if it's supposed to be able to reason?" What would you tell them?

See solution

The agent didn't "ignore" anything on purpose — it never had the option to do otherwise. The channel field was never marked with $fromAI(), so it was never part of the input_schema the model receives: for the model, this tool has a single field to fill in, message. There's no mechanism by which something written in the chat could change a value that isn't exposed as part of the form. The behavior is exactly what's expected, and it's good design news, not a failure: it means that channel is a fixed value no matter what happens in the conversation.

Why it works: the right question in the face of any "the agent didn't do what the customer asked" isn't "why did the model decide to ignore it?" but "was that field even exposed to its decision?" — the distinction between $fromAI() and a fixed value you saw in this lesson.

Exercise 3 — Diagnosis with the input in view. The worked example's TuTienda agent responds "I couldn't find any order with that number" to a customer who asked about their order "# 4521" (with a space between the symbol and the number, a customer typo). In the execution panel, the HTTP Request node shows it received order_id: "4521" — with neither the # nor the space — and that the API responded 404 Not Found. Is the problem in how the model filled the field, or in how the tool executed the call? Justify your answer.

See solution

Neither one failed. The model filled the field correctly according to its instruction: the description said "digits only, without the # symbol," and "4521" meets exactly that — the extra space in the customer's message didn't change the result, because the model was already cleaning up the format as instructed. The 404 isn't the tool mis-executing correct data either: it's that order 4521 genuinely doesn't exist in TuTienda's system with that exact id (maybe the customer got the number wrong, or the real order is a different one). The customer's own input data was what didn't correspond to a real order.

Why it works: checking the real input before assuming a cause is exactly this lesson's third common mistake's habit — and in this case, checking it also lets you rule out two suspects (model and tool) at once, instead of only confirming one.

Summary and next step

You can now name the three parts that make up any tool — name, description, input_schema — and you know the node's Description field in n8n is literally that object's description, while every $fromAI(...) you write declares one more field in input_schema. You can now trace the cycle's complete JSON: the model reads those three parts of every available tool, decides which applies, produces a tool_use block with the arguments it extracted from the conversation following each field's description, and n8n — not the model — executes the real action against the real system and returns the result so the model can reason once more. And you now know how to tell apart, with the real input in view, whether a failure comes from how the model decided or from how the tool executed.

Before moving on you should be able to: name a tool's three parts without looking back at this lesson; given a node field, say whether it's part of what the model can decide or whether it's fixed, just by checking whether it has $fromAI() or not; and, given a tool_use's real input, tell apart whether a failure is one of filling (the model) or execution (the tool).

With the complete mechanism in hand, the next lesson stops being abstract: you're going to go through n8n's native tool catalog — search, create, send — and connect them to a real agent, knowing exactly what you're exposing to the model every time you mark a field with $fromAI().

Resources

  • Tool use with Claude — Claude Docs — the source for a tool's anatomy (name, description, input_schema) and the complete tool_use → execution → tool_result round trip, with the get_weather example cited in this lesson.
  • How tools work — n8n Docs — how n8n describes tools' role and the catalog of nodes available to connect to ai_tool.
  • Use AI for parameters ($fromAI) — n8n Docs — complete reference for the $fromAI(key, description, type, defaultValue) function used in this lesson's worked example.
  • Call n8n Workflow Tool node — n8n Docs — a concrete example, documented by n8n, of the Description field guiding the agent's decision and $fromAI() filling in the input parameters.
  • AI Agent node — n8n Docs — reference for the node and the ai_tool connection, already cited in Module 1 and which you keep using through the rest of this module.