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

3. Native n8n tools: search, create, send

Description

By the end of this lesson you'll be able to take an action node you already know from building normal workflows — the Slack node, in this lesson's example — and connect it directly to the agent as its own tool, restricted to a single operation (search, create, or send), with a description that tells the model when to use it and with $fromAI() filling in only the data that changes on every execution.

This matters because it's, by far, the most common way to give an agent hands in production. You don't need to write code or spin up your own server: almost any node you'd already use in a normal workflow — Slack, and in the next lesson Gmail, Sheets, your database — can turn into a tool with the same configuration you already know how to do, plus three new decisions this lesson teaches you to make well. According to the market validation behind this guide, agents that act on real systems show up in 57% of postings that ask for these skills — and that "acting" almost always starts exactly here: with a native node connected as a tool, not with an integration built from scratch.

Connection to the module: in the previous lesson you saw the tool calling mechanism — how the model decides which tool to invoke, with what data, and what it does with the result. This lesson doesn't repeat that mechanism; it puts it into practice with the simplest kind of tool to build: a native n8n node restricted to one operation. You still won't connect full business systems with their own complex credentials — Gmail, Sheets, a database, an external API via HTTP —, that's exactly the next lesson's job.

From workflow node to agent tool

Imagine handing someone new on the team a whole keyring with fifteen unlabeled keys, and telling them "one of these opens the supply room, figure it out." They're going to waste time trying, and in the worst case they open the wrong door. Now imagine instead you hand them a single key, with a label that says exactly "supply room — use only to restock inventory." There's no ambiguity: they know what that key opens and when to use it, and they don't have access to the other fourteen doors even if they wanted to.

Connecting a native n8n node as an agent's tool is literally that: you hand over one key at a time, not the whole keyring. n8n allows this with almost any action node you already know — every compatible node's documentation says so explicitly: "This node can be used to enhance the capabilities of an AI agent," and clarifies that, used this way, "many parameters can be set automatically, or with information directed by AI." The concrete mechanism: you open the AI Agent node's Tools connector — that opens the Tools Panel, a node search box just like the one you use to build any workflow — and there you choose the node you want to hand over as a tool.

The difference from using that same node in a normal workflow is that, when you connect it as a tool, you have to make three decisions that don't exist in a linear workflow:

  1. What exact operation this instance of the node does. A tool isn't "the Slack node" in the abstract — it's a fixed operation, like Search or Send. If you need the agent to be able to search AND send, you connect two instances of the node, each restricted to its own operation.
  2. Which parameters you fix, and which the model fills in. Any field can stay at a fixed value — like in a normal workflow — or be wrapped in $fromAI(key, description, type, defaultValue), the same function you saw in the previous lesson, so the model completes it at runtime based on the user's request. n8n even saves you from writing the expression by hand: next to every compatible parameter there's a small star icon — turn it on and n8n writes the $fromAI() for you, using the parameter's name as the key. For a quick prototype that's enough; for a tool you're going to leave in production, it's worth writing the description and the type yourself, because they're exactly the clue the model uses to get it right.
  3. What name and what description you give that tool. The model doesn't see inside the node — it decides which tool to call by reading its name and its description, exactly as you learned in the previous lesson about how the model chooses between several tools. Two Slack tools with generic names like "Slack" and "Slack" are, for the model, nearly indistinguishable.

Notice something important about the module's vocabulary: n8n also offers nodes built specifically to be tools, which don't exist outside that role — Wikipedia, SerpAPI, Custom Code Tool, HTTP Request Tool, Call n8n Workflow Tool, among others. Those come later, spread across different lessons in this module. This lesson and the next cover the other half of the map: normal action nodes, the same ones you already know how to configure, connected to the agent in tool mode.

Worked example

TuTienda's operations team wants an agent to watch for rejected payments and alert Slack without anyone having to type the alert by hand. The workflow starts with a Webhook that receives alerts from the payments system:

# Payload arriving at the Webhook when a payment fails several times in a row
{
  "orderId": "8834",
  "failedAttempts": 3,
  "reason": "card declined by issuing bank"
}

That payload turns into the message the agent receives: "Order #8834 had 3 rejected payment attempts. Reason: card declined by issuing bank." The agent has three tools connected to the Tools connector, all three on the Slack node, each restricted to a single operation:

# Tool 1 — search_slack_messages
Resource     = "Message"                                                    # fixed: this tool only searches
Operation    = "Search"                                                     # fixed: never sends or deletes
Channel      = "#payment-incidents"                                          # fixed: scope limited by you
Search Query = {{ $fromAI("searchQuery", "Incident keywords, for example the order number", "string") }}
Description  = "Check whether this order has already been discussed in the payment incidents channel before opening something new."
# Tool 2 — create_slack_channel
Resource     = "Channel"                                                    # fixed
Operation    = "Create"                                                     # fixed: never archives or renames
Channel Name = {{ $fromAI("channelName", "Lowercase name with hyphens, format incident-payments-<orderId>", "string") }}
Description  = "Create a dedicated channel when a payment incident has no previous channel."
# Tool 3 — send_slack_message
Resource    = "Message"                                                     # fixed
Operation   = "Send"                                                        # fixed: never searches or deletes
Channel     = {{ $fromAI("channel", "Channel to notify, usually the incident channel just created", "string") }}
Text        = {{ $fromAI("messageText", "Clear summary of the incident for the team", "string") }}
Description = "Send an alert message to an already-existing Slack channel."

What to expect. The agent receives the alert for order #8834 and reasons in this order, applying the same tool-calling cycle you saw in the previous lesson:

1. Calls search_slack_messages with searchQuery = "8834"
   → Result: 0 messages found in #payment-incidents.
2. Since there's nothing previous, calls create_slack_channel
   with channelName = "incident-payments-8834"
   → Result: channel #incident-payments-8834 created.
3. Calls send_slack_message with
   channel = "#incident-payments-8834"
   messageText = "Order #8834: 3 rejected payment attempts (card declined by issuing bank). Follow up with the customer."
   → Result: message sent.
4. Responds to the workflow: "I opened channel #incident-payments-8834 and alerted the team."

Interpretation: each call used a different tool because each one had an unambiguous operation and description — the model never had to guess whether "search" and "send" were the same action with different parameters, because literally they aren't: they're three separate nodes. And none of the three could step outside its lane — search_slack_messages has no way to send anything, even pointed at the same channel — because you fixed Resource and Operation, not the model.

What you fix and what you leave to the model

The configuration above hides a pattern worth naming separately, because you're going to repeat it with every native node you connect as a tool, in this lesson and the next.

ParameterFixed or $fromAI()?Why
Resource and OperationFixed, alwaysDecide WHAT the tool can do. Leaving it to the model means a single tool could end up searching, creating, or sending depending on what suits it — you lost control over the operation from the design stage.
Channel in search_slack_messagesFixedLimits WHERE it searches. Without this limit, the agent could search any channel in the company, including one it shouldn't be checking.
Search Query, Channel Name, Text$fromAI()These are the DATA that changes on every execution — the order number, the reason, the summary. The model knows them because they come in the message it received; you can't fix them in advance because you don't know which order is going to fail tomorrow.

The short rule: everything that decides what the tool can do you fix yourself when designing it; everything that decides what data that concrete action runs with you leave to the model. Lesson 5 of this module is going to formalize this same idea with a name — tool contracts and trust boundaries — and take it further than Resource/Operation. For now, this rule is enough to keep any of your tools from doing something you didn't decide it could do.

Common mistakes

Leaving Resource or Operation to $fromAI() instead of fixing them (conceptual). What happens: someone wraps Resource and Operation in $fromAI() thinking "this way the agent is more flexible, it can decide the whole action," and ends up with a single Slack tool that sometimes searches, sometimes sends, and, if the prompt pushes it hard enough, even deletes a message — all from the same node, with nobody having explicitly authorized it for each case. Why it happens: it's tempting to treat $fromAI() as a generic way to "make everything dynamic," but the function is meant for an action's data, not for the action itself. How to spot it: check which fields on each tool have $fromAI() — if Resource or Operation show up there instead of a fixed value, that tool can execute more than its name promises. How to fix it: fix Resource and Operation to a concrete value per tool instance; if you need several actions, add several instances, each with its own name and description.

Giving generic names or descriptions to several tools from the same node (conceptual). What happens: you connect search_slack_messages, create_slack_channel, and send_slack_message, but leave all three with the default name n8n proposes — something generic like "Slack" — or nearly identical descriptions ("interacts with Slack"). The agent, faced with a request to "alert the team," ends up invoking the wrong tool, or tries to use search_slack_messages to send a message. Why it happens: the model doesn't read inside the node to know what each tool does — it reads exactly the name and description you wrote, just as you learned in the previous lesson about how the model decides which tool to invoke. If three tools look nearly identical from the outside, for the model they are. How to spot it: check the agent's execution trace — the same one you used in the previous lesson — and compare which tool it invoked against which one you needed for that request; repeated names or descriptions across tools are the warning sign before it fails. How to fix it: name every tool with a clear verb and object (search_slack_messages, not slack_tool_1) and write a description that states, in one sentence, when to use that tool and not another.

Testing the node in isolation while it has fields with $fromAI() (practical). What happens: you want to quickly test that create_slack_channel's configuration works, you click "Test step" directly on the Slack node, outside the agent's flow, and the execution fails or the Channel Name field comes back empty. Why it happens: $fromAI() only makes sense — and only resolves — when the node is connected to an AI Agent's Tools connector and it's the agent invoking it during its reasoning; n8n's documentation says so plainly: the function "is only available for tools connected to the AI Agent node." Tested in isolation, there's no agent deciding what value to put there. How to spot it: if a node with $fromAI() in its fields fails or produces empty values when tested alone, check whether you're triggering it outside the agent. How to fix it: to test a tool with $fromAI(), run the complete AI Agent with a test message that triggers that tool — not the isolated node.

Exercises

Exercise 1 — Predict the correct tool. An operations agent has the same three tools from the worked example connected (search_slack_messages, create_slack_channel, send_slack_message), each with its description as configured. This message comes in: "Channel #incident-payments-8834 already exists, let them know there that the problem was resolved." Which tool (or tools) should the agent invoke, and in what order? Justify your answer using the descriptions, not a guess.

See solution

Only send_slack_message, once. The message already states the channel exists — no need to check whether there's a previous channel (search_slack_messages is for when it's unknown whether the incident's already been discussed) nor to create a new one (create_slack_channel is for when there's no previous channel). The request matches exactly send_slack_message's description: "Send an alert message to an already-existing Slack channel." The agent would fill in channel = "#incident-payments-8834" and messageText with a summary that the problem was resolved.

Why it works: each tool's description is what the model compares against the request — when the message already resolves the ambiguity the search tool exists to resolve, there's no reason to invoke it.

Exercise 2 — Find the risky design. Review this Slack tool configuration connected to an agent and explain what's wrong, using what you saw in "Common mistakes":

# Tool — manage_slack
Resource    = {{ $fromAI("resource", "Message or Channel depending on what's needed", "string") }}
Operation   = {{ $fromAI("operation", "Whatever operation the agent considers necessary", "string") }}
Channel     = {{ $fromAI("channel", "Channel to operate on", "string") }}
Text        = {{ $fromAI("text", "Message content if applicable", "string") }}
Description = "Interacts with Slack as needed by the agent."
See solution

Resource and Operation are wrapped in $fromAI() instead of fixed — this tool isn't restricted to searching, creating, or sending: it can end up executing any operation available on the Slack node, including deleting a message or archiving a channel, if the model decides it's "needed." On top of that, the Description is generic ("as needed by the agent"), which gives the model no real criterion for deciding when to use this tool versus others — and if there's more than one Slack tool connected, this ambiguity makes the second common mistake worse.

The fix: split this into separate tools, one per concrete operation (like in the worked example), with Resource and Operation fixed and a specific description of when to use each one.

Why it works: a well-designed tool has an action surface you can describe in one sentence without using the word "depending" — if you need that word, it's a sign it's actually several tools disguised as one.

Exercise 3 — Design a fourth tool. TuTienda's operations team wants to add a fourth capability: the agent should be able to invite the finance on-call person (Slack user @finance-oncall) to the incident channel every time it creates a new channel. Using the Channel resource and the Invite operation you saw in this lesson, write the tool's complete configuration — what fields you fix, what fields you leave in $fromAI(), and a description — following the same pattern as the example's three tools.

See solution
# Tool 4 — invite_user_to_incident_channel
Resource    = "Channel"                                                     # fixed
Operation   = "Invite"                                                      # fixed: never creates or archives
Channel     = {{ $fromAI("channel", "Incident channel to invite to, usually the one just created", "string") }}
User        = "@finance-oncall"                                           # fixed: always the same on-call person
Description = "Invite the finance on-call person to a newly created payment incident channel."

Resource and Operation stay fixed, same as the other three tools — this tool only does one thing. Channel goes in $fromAI() because it changes with every incident. User stays fixed, not dynamic, because the specific requirement is always the same on-call person — there's no point letting the model "guess" who to invite when the business already decided it's always the same account.

Why it works: not everything that theoretically varies needs $fromAI() — it does vary per incident, yes, but the correct value (@finance-oncall) is a fixed business decision, not a piece of data the incoming message informs the agent about.

Summary and next step

You now know how to connect a native n8n action node — the same one you'd use in any workflow — to an AI Agent's Tools connector, restricting it to a single operation with Resource and Operation fixed, leaving only the data that changes on every execution in $fromAI(), and writing it a name and description that let the model choose the right tool among several.

Before moving on you should be able to: explain why Resource and Operation should almost never go in $fromAI(); design, for any native node, a set of tools where each has a single operation and an unambiguous description; and tell apart, for any new parameter you see, whether it's yours to fix or the model's to decide.

What you haven't seen yet is what this same thing looks like when the system on the other end isn't Slack, but the business systems that genuinely run a company — email, a shared spreadsheet, your own database, or any API with no dedicated node in n8n. With this lesson's pattern already internalized, that's exactly what comes next in the following lesson.

Resources

  • AI Agent node — n8n Docs — reference for the AI Agent node, including the Tools connector you opened in this lesson.
  • How tools work — n8n Docs — the catalog of tool sub-nodes built specifically for agents (Wikipedia, SerpAPI, Call n8n Workflow Tool, Custom Code Tool, HTTP Request Tool) versus the normal action nodes you used here.
  • Use AI for parameters — n8n Docs — the complete reference for $fromAI(key, description, type, defaultValue) and the star icon that generates the expression for you.
  • Slack node — n8n Docs — the complete list of resources and operations on the node you used in the worked example, including the ones you didn't cover (Get permalink, Update, among others).
  • Gmail node — n8n Docs — a second example of the same notice you're going to see on any node compatible with tool mode ("This node can be used to enhance the capabilities of an AI agent"), as a preview of the next lesson.
  • Data tables — n8n Docs — n8n's native storage, with no external credentials, with search and row-creation operations — useful if you want to practice this lesson's same pattern without depending on a Slack account.