# 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: 1. It gets the system prompt, conversation, and the list of tools. 2. For each tool it reads name + description + parameter schema (with `$fromAI` descriptions). 3. 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 1. **What it does** (one sentence). 2. **When to use it** (one or two sentences, with boundaries / examples). 3. **Parameter notes** (only if not already covered in `$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. --- ## 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 Image` vs `Edit Image` share most logic → collapse to one with an `imageName` parameter (empty = generate, populated = edit). - **Two tools only when genuinely distinct AND the descriptions clearly differentiate.** `Send DM` vs `Send Channel Message` are 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**