11 KiB
Agent tools
The agent picks tools by reading their name and description — nothing else. Both are part of the prompt. Treat tool design like API design: what it does, when to use it, what each parameter means, and how it fails.
The four tool types
1. Native tool node
Pre-built tool versions of regular nodes: slackTool, gmailTool, googleSheetsTool, toolCalculator, notionTool, httpRequestTool, and so on. Identical to their non-tool counterparts except parameters can be agent-filled via $fromAI().
- Pros: minimal config, well-tested, native feel.
- Cons: one node = one operation. Multi-step logic doesn't fit.
- Use when: the capability maps cleanly to one node and one operation.
When a native node is missing an operation or needs a non-standard param shape, point an HTTP Request Tool at the service's API with the service's predefined credential type — you reuse the existing OAuth/API-key credential and get the full API.
2. Sub-workflow as tool (@n8n/n8n-nodes-langchain.toolWorkflow)
The default for anything beyond one node. Any workflow becomes a tool with typed $fromAI() inputs.
- Pros: full power of n8n inside the tool — branching, error handling, sub-sub-workflows, native nodes, custom logic. Reusable across agents. Independently testable.
- Cons: one extra workflow boundary, slight latency.
- Use when: more than one node, logic that might be reused, or you want testability.
The canonical n8n way to build agent capabilities. → SUBWORKFLOW_AS_TOOL.md
3. HTTP Request Tool (@n8n/n8n-nodes-langchain.toolHttpRequest)
A wrapper around the HTTP Request node exposing its parameters to the agent.
- Pros: any HTTP API becomes a tool with one node.
- Cons: HTTP only. Auth/retry/error handling are yours to wire.
- Use when: calling a single external API the agent should orchestrate directly.
One thing to know: HTTP Request has its own HTTP-level timeout (default 5 minutes) — bump options.timeout for slow endpoints. The agent tool itself has no timeout; the agent waits as long as the tool takes. Pointing it at, say, the Notion API (with the Notion predefined credential) lets the agent compose path, method, and body itself — covering operations the native node doesn't expose. Trade-off: the agent is now writing API requests, which is more error-prone and needs a capable model plus clear endpoint guidance in the description. That widens the blast radius — make sure the user understands.
4. MCP Client Tool (@n8n/n8n-nodes-langchain.mcpClientTool)
Connects the agent to any MCP server. Two flavors:
-
External MCP servers — any third-party or self-hosted MCP (GitHub, Linear, Notion, custom internal). One node exposes every tool that server offers.
-
n8n-hosted MCP — a workflow on the same instance published with MCP access enabled. Same client node, pointed at an n8n MCP trigger URL. Lets one workflow serve many agents.
-
Cons: tool descriptions and shapes come from the server, so quality varies and you can't easily tune them. Auth and reachability are yours.
-
Use when: a maintained MCP server already covers the capability, or you want one published workflow to serve many agents.
Plus: Custom Code Tool (@n8n/n8n-nodes-langchain.toolCode)
Pure inline computation (math, parsing, formatting). Its runtime contract is string in / string out, no $fromAI, no $helpers and is owned by the n8n-code-tool skill — read it before writing one. Rule of thumb: if you want $fromAI() in the code, you want .toolWorkflow instead.
Decision: which tool type?
Capability the agent needs?
├── One native node + one operation does it
│ → native tool node
├── Native node missing an op / needs custom params for ONE API
│ → HTTP Request Tool (with the service's predefined credential)
├── More than one node, or logic that might be reused
│ → Sub-workflow as tool (.toolWorkflow) ← default when in doubt
├── Pure deterministic computation, one-off, inline
│ → Custom Code Tool (.toolCode) ← see n8n-code-tool
└── A maintained MCP server covers it / publish n8n logic to many agents
→ MCP Client Tool
$fromAI(): how the agent fills tool parameters
$fromAI() is a real n8n expression helper, written inside a tool node's parameter expressions. Parameters the agent should decide get wrapped in it:
sendTo: ={{ $fromAI('recipient', 'Email address of the recipient', 'string') }}
subject: ={{ $fromAI('subject', 'Email subject line, concise and informative', 'string') }}
body: ={{ $fromAI('body', 'Email body in plain text, professional tone', 'string') }}
Shape: $fromAI(paramName, description, type?, defaultValue?)
- paramName — the name the model uses internally. snake_case or camelCase, be consistent.
- description — what value to produce. Part of the prompt. Be specific: format, range, example.
- type —
'string'(default),'number','boolean','json'. Enforced — a wrong-typed value fails the call. - defaultValue — used when the model omits the parameter.
It carries JSON only — it cannot carry binary (no base64, no file bytes), even through a non-AI binding. For binary, pass a storage key as a string and have the tool re-fetch (→ n8n-binary-and-data).
A good description vs a useless one:
✅ ={{ $fromAI('imageName', 'Storage key for an existing image to edit, or empty for a new generation. Use the exact key shown in the system prompt; do not reconstruct or guess.', 'string') }}
❌ ={{ $fromAI('imageName', 'image name', 'string') }} // useless to the model
Treat $fromAI descriptions like JSDoc — the model reads them to figure out what to pass.
Plumbed params: hide what the agent shouldn't decide
Not every parameter has to be $fromAI. Any parameter can be filled deterministically from workflow context, and plumbed values are invisible to the agent — not in the tool schema, not influenceable by anything the model produces:
reason: ={{ $fromAI('reason', 'Why the user is requesting a refund', 'string') }} // agent-filled
customerId: ={{ $('Chat Trigger').first().json.user.id }} // hidden
maxRefund: ={{ $('Get user tier').first().json.refundLimit }} // hidden
idempotencyKey:={{ $('Chat Trigger').first().json.sessionId }} // hidden
Plumb anything the agent shouldn't get wrong or see:
- Identity —
userId,customerId, authenticated actor, tenant scope. - Authority limits — refund caps, tier flags, allowed regions.
- Correlation IDs —
sessionId, idempotency keys, trace IDs.
Give the agent a button to push, not a steering wheel. The strongest version is a sensitive tool with zero $fromAI parameters: a "Refund order" tool takes orderId from the trigger, amount from the fetched order record, actor from the session — all plumbed. The agent literally cannot refund the wrong order; it only chooses whether to fire. Pair with HUMAN_REVIEW.md for actions needing both deterministic params and sign-off.
Tool name and description as prompt
Selection process the model runs every turn:
- It gets the system prompt, conversation, and the list of tools.
- For each tool it reads name + description + parameter schema (with
$fromAIdescriptions). - It picks the tool whose description best matches what it needs to do.
Bad names and descriptions cause bad selection — usually silently. The model just doesn't call your tool, or calls a different one with garbage parameters. No error.
Names: verb-first and specific
| Good | Bad | Why |
|---|---|---|
Search customer database |
query / tool1 |
Generic names say nothing |
Generate image with Veo |
imageGen |
Which generator? |
Edit existing image |
edit |
Edit what? |
Send Slack message to channel |
slack |
Name the action, not just the surface |
Lookup user by email |
getUser |
Lookup how? |
Descriptions: three parts
- What it does (one sentence).
- When to use it (one or two sentences, with boundaries / examples).
- Parameter notes (only if not already covered in
$fromAIdescriptions).
Edit existing image: Modifies an image the user already uploaded, based on a prompt.
Use when the user uploaded an image and asks for changes (color, style, composition, content).
Do NOT use for generating new images from scratch — use Generate Image for that.
The imageName parameter must be the storage key of the existing image as listed in your
available files; do not pass the original filename or a URL.
That description does work that would otherwise bloat the system prompt — which is exactly the point.
Tool descriptions as modular prompts
Anything specific to how to call this tool belongs in the tool's description, not the system prompt:
| In the system prompt (move out) | Better in the tool description |
|---|---|
"When generating images, prefer realistic photography over 8k cinematic" |
Generate Image: "Default to realistic photography aesthetics…" |
| "If the search tool returns nothing, summarize politely" | Search: "Returns up to 10 results; if empty, report 'no matches' rather than retrying broader" |
| "Use 9:16 for video tools" | Generate Video: "Defaults to 9:16; pass aspectRatio: '16:9' for landscape" |
Three reasons: reusability (the tool teaches each new agent how to use it), token efficiency (per-tool guidance only loads when the model considers that tool, not every turn), maintainability (one description, not a buried paragraph).
Granularity: one tool with branching, not two near-identical tools
The model gets confused choosing between near-identical tools. If two are ~80% the same internally:
- One tool with a branching parameter.
Generate ImagevsEdit Imageshare most logic → collapse to one with animageNameparameter (empty = generate, populated = edit). - Two tools only when genuinely distinct AND the descriptions clearly differentiate.
Send DMvsSend Channel Messageare distinct.
Operational notes
- maxIterations. Agents have a configurable tool-call cap (
options.maxIterations), and the default is low. A multi-tool agent that chains calls hits it and surfaces "max iterations reached" or empty output. Raise it. Build a fallback — don't trust graceful recovery. - Tool-call cost. Each call is at minimum one extra model round-trip. Frequently-called tools should return concise results — bloated returns burn input tokens fast.
- Tool failure handling. Set
onError: 'continueErrorOutput'on tool sub-workflows where you want the agent to receive an error string instead of halting; the agent can retry, switch tools, or report. → n8n-error-handling.
Cross-references
- The sub-workflow tool pattern in detail → SUBWORKFLOW_AS_TOOL.md
- System-prompt-vs-tool-description split → SYSTEM_PROMPT.md
- Passing binary into tools → n8n-binary-and-data
- The Custom Code Tool contract → n8n-code-tool