data/skills/n8n-agents/TOOLS.md
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.
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().
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.
@n8n/n8n-nodes-langchain.toolWorkflow)The default for anything beyond one node. Any workflow becomes a tool with typed $fromAI() inputs.
The canonical n8n way to build agent capabilities. → SUBWORKFLOW_AS_TOOL.md
@n8n/n8n-nodes-langchain.toolHttpRequest)A wrapper around the HTTP Request node exposing its parameters to the agent.
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.
@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.
@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.
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?)
'string' (default), 'number', 'boolean', 'json'. Enforced — a wrong-typed value fails the call.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.
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:
userId, customerId, authenticated actor, tenant scope.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.
Selection process the model runs every turn:
$fromAI descriptions).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.
| 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? |
$fromAI descriptions).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.
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).
The model gets confused choosing between near-identical tools. If two are ~80% the same internally:
Generate Image vs Edit Image share most logic → collapse to one with an imageName parameter (empty = generate, populated = edit).Send DM vs Send Channel Message are distinct.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.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.