N8n Agents
Romuald CzlonkowskiCommunityCreated by Romuald Czlonkowski
- Skill
- Guidelines
- Automation and integrations
- MIT
- No issues found
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
This skill provides technical best practices for building AI-powered workflows in n8n. It covers the architectural design of AI agents, including the proper use of sub-nodes for models, memory, tools, and output parsers. It emphasizes the importance of tool descriptions, structured output with auto-fixing, and human-in-the-loop patterns to ensure reliable agent behavior. The guide also distinguishes between standard AI Agent nodes and persisted agents managed via the n8n agents module, offering clear patterns for chat-based bots and RAG implementations.
What it does
- Defines patterns for selecting between AI Agents, chains, classifiers, and extractors.
- Explains the sub-node architecture for connecting models, memory, tools, and parsers.
- Provides rules for writing effective tool names and descriptions for LLM tool calling.
- Details the implementation of structured output with auto-fixing to prevent workflow failures.
- Outlines strategies for human-in-the-loop review for sensitive or destructive tool actions.
- Covers the management of persisted agents using the n8n agents module.
When to use it
- When building or editing AI nodes in n8n workflows.
- When implementing chat assistants or bots that require memory and tool usage.
- When designing workflows that require structured JSON output from an LLM.
- When integrating RAG or external tools into an agentic workflow.
- When setting up human-in-the-loop approval gates for automated actions.
Advantages
- Reduces common anti-patterns like agent-switch loops or poorly defined tools.
- Improves agent reliability through structured output and auto-fixing.
- Ensures consistent memory management and session handling.
- Provides clear guidance on separating system prompts from tool-specific instructions.
- Offers a standardized approach to building multi-step agentic workflows.
Usage examples
- “How do I decide between an AI Agent node and a Text Classifier node?”
- The guide explains that an AI Agent is for multi-turn reasoning and tool calling, while a Text Classifier is a more efficient, single-node solution for routing natural language inputs into specific branches.
- “How should I handle human approval for an agent that sends emails?”
- The guide recommends placing a human-in-the-loop review node between the tool and the agent, ensuring the human reviews the exact parameters before the tool executes.
- “Why is my agent failing to use the tools I provided?”
- The guide highlights that tool names and descriptions are part of the prompt; if they are vague or empty, the model cannot effectively select or use them.
- “How do I ensure my chat bot does not enter an infinite loop?”
- The guide instructs to filter out the bot's own user ID at the trigger or the first node to prevent the bot from responding to its own messages.
Install
Download the skill as a ZIP:
https://autogineer.com/agent-skills/czlonkowski/n8n-agents/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip n8n-agents.zip -d .agents/skills
Documentation by Romuald Czlonkowski
n8n Agents
The n8n AI Agent node (@n8n/n8n-nodes-langchain.agent) is a multi-turn LLM driver with sub-nodes for the model, memory, tools, and an optional output parser. This skill is the deep guide to designing agents and the LangChain family around them. For the high-level "where an agent fits in a workflow" picture, see n8n-workflow-patterns ai_agent_workflow.md — this skill goes one level down into how to build it well.
For node-type formats: in workflow JSON the LangChain nodes use the long @n8n/n8n-nodes-langchain.* form (.agent, .lmChatOpenAi, .memoryBufferWindow, .outputParserStructured, .toolWorkflow, .toolHttpRequest, .toolCode). When you call get_node / validate_node, use the short form (nodes-langchain.agent). See n8n-mcp-tools-expert for the format rules.
Pick the right node first
Reaching for an Agent when the task is one-shot classification or extraction is the most common over-build. Decide before you wire anything:
| You need to… | Use | Why |
|---|---|---|
| Call tools, reason over multiple turns, or hold memory | AI Agent (.agent) | The full loop: model + tools + memory + optional parser. Also a fine default when you'd rather standardize. |
| One-shot text in → text out, no tools | Basic LLM Chain (.chainLlm) | No agent loop, easier to debug. Still accepts an outputParserStructured sub-node. |
| Route a natural-language input to one of N branches | Text Classifier (.textClassifier) | ONE node, N output handles, downstream wires directly into each. Not Agent + Switch. |
| Pull structured fields out of free text | Information Extractor (.informationExtractor) | Purpose-built field extraction with a schema. |
| 3-way positive/neutral/negative split | Sentiment Analysis (.sentimentAnalysis) | Built-in branch outputs. |
| Condense a long document | Summarization Chain (.chainSummarization) | Map-reduce summarization built in. |
| Generate an image / audio / video | The provider's native single-call node (OpenAI, Gemini, ElevenLabs…) | NEVER wrap media generation in an Agent — see "Binary and the agent boundary". |
Text Classifier detail (the Agent + Switch anti-pattern): every category needs both a name AND a description. The model routes against the description, not the name — a category with no description gets picked by coin-flip. Set options.enableAutoFixing: true for robustness on edge inputs. One node, N branches, done. Reaching for an Agent that "decides" then a Switch that "routes" is two nodes plus prompt boilerplate for what Text Classifier does natively.
Chat-model nodes (.lmChatOpenAi, .lmChatAnthropic, .lmChatOpenRouter, …) are sub-nodes — they don't run standalone. They wire into a chain, agent, classifier, or extractor via the ai_languageModel connection.
The sub-node pattern
The Agent has a main input (the prompt / user message) and up to four sub-node slots, each wired by its own ai_* connection type:
| Slot | Connection type | Required? | Node example |
|---|---|---|---|
| model | ai_languageModel | Yes | .lmChatOpenAi, .lmChatAnthropic, .lmChatOpenRouter |
| memory | ai_memory | Optional | .memoryBufferWindow, .memoryPostgresChat |
| tools | ai_tool | Optional (but the point of an agent) | slackTool, .toolWorkflow, .toolHttpRequest, .toolCode |
| outputParser | ai_outputParser | Optional | .outputParserStructured |
A sub-node connects FROM itself TO the agent. In workflow JSON the connection lives on the sub-node, keyed by the ai_* type:
"Main LLM": {
"ai_languageModel": [[{ "node": "AI Agent", "type": "ai_languageModel", "index": 0 }]]
},
"Simple Memory": {
"ai_memory": [[{ "node": "AI Agent", "type": "ai_memory", "index": 0 }]]
},
"Search customer DB": {
"ai_tool": [[{ "node": "AI Agent", "type": "ai_tool", "index": 0 }]]
}Multiple tools all connect into the same ai_tool index 0 — they stack, they don't fan into separate indices. With n8n_update_partial_workflow you wire each with an addConnection op using sourceOutput: "ai_tool". The agent puts its final answer in $json.output (not .text, not .response) — downstream nodes read {{ $json.output }}.
See EXAMPLES.md for a complete stateless agent-core node-object snippet.
Two non-negotiables
Tool names and descriptions ARE part of the prompt. The model picks a tool by reading its name and description — nothing else. A tool named
tool1with an empty description is invisible to the model: it skips it, mis-selects it, or hallucinates parameters. There's usually no error — just an agent that "won't use my tool". Treat both like API design. → TOOLS.mdStructured output must parse AND autoFix. An
outputParserStructuredwithautoFix: trueand a coding-capable fixer model is the production pattern. Without autoFix, one malformed JSON response halts the whole workflow. → STRUCTURED_OUTPUT.md
Strong defaults
Per-tool usage goes in the tool description, not the system prompt. Anything about how to call this specific tool belongs with the tool, so it travels across agents and keeps the system prompt focused. → SYSTEM_PROMPT.md
Sub-workflow tools (
.toolWorkflow) for anything multi-step. Any workflow becomes a tool with typed$fromAI()inputs, and composes with branching, error handling, and reuse. Default here when in doubt. → SUBWORKFLOW_AS_TOOL.md and n8n-subworkflows.Wrap tools with user-visible side effects in human review. Sends, payments, refunds, account changes get gated behind an approval node so a human signs off before the tool fires. → HUMAN_REVIEW.md
Raise
maxIterations. The default tool-call cap is low (single digits on most versions) — fine for a one-tool agent, far too low for a multi-tool agent that chains several calls per turn. It surfaces as "max iterations reached" or empty output. Setoptions.maxIterationsto a realistic ceiling (15 for a focused sub-agent, 50-200 for a broad orchestrator).Put the current date in the system prompt via
{{ $now }}(or{{ $now.format('DDDD') }}). A hardcoded date is stale immediately.
The four tool types
Pick the lightest option that covers the job:
| Tool type | Node | Use when |
|---|---|---|
| Native tool node | slackTool, gmailTool, toolCalculator, … | The capability maps to one existing node + one operation. Lowest overhead. |
| Sub-workflow as tool | .toolWorkflow | More than one node, reusable logic, or you want independent testability. The canonical n8n way — default when in doubt. |
| HTTP Request Tool | .toolHttpRequest | A single external HTTP API the agent should orchestrate directly. Reuse the service's predefined credential to cover operations a native node doesn't expose. |
| MCP Client Tool | .mcpClientTool | A maintained MCP server already covers it, or you want one published workflow to serve many agents. |
There is also a Custom Code Tool (.toolCode) for pure inline computation — but its runtime contract (string in / string out, no $fromAI, no $helpers) is owned by the n8n-code-tool skill. Read that before writing one. Rule of thumb: if you find yourself reaching for $fromAI() inside the code, you want .toolWorkflow instead.
$fromAI(): how the agent fills tool parameters
Tool parameters the agent should decide are wrapped in $fromAI(). It is a real n8n expression helper, used inside a tool node's parameter expressions:
={{ $fromAI('paramName', 'what to put here — be specific: format, range, example', 'string') }}paramName — the name the model uses internally (snake_case or camelCase, be consistent).
description — tells the model what value to produce. It is part of the prompt — write it like JSDoc.
type (optional) —
'string'(default),'number','boolean','json'. A wrong-typed value fails the call.defaultValue (optional) — used when the model omits it.
$fromAI() carries JSON only — it cannot carry binary (no base64, no file bytes). And not every parameter has to be $fromAI: plumb identity, authority limits, and correlation IDs (userId, refund caps, sessionId) deterministically from workflow context so the agent can't get them wrong or even see them. → TOOLS.md for the full anatomy and the "give the agent a button, not a steering wheel" pattern.
System prompt vs tool description
| Belongs in the system prompt | Belongs in the tool's description |
|---|---|
| Persona, role, voice | What this specific tool does |
| Global output/format rules ("respond in markdown") | When to use it vs other tools |
| Refusal / safety behavior | What each parameter means and its shape |
Display protocols (![]() for images) | Examples of good vs bad invocations |
Universal context (current date via $now, user role) | Tool-specific gotchas (rate limits, edge cases) |
| Inter-tool flow ("after generating, always display") | Tool-specific input transformations |
Why split it: a well-described tool works in any agent that drops it in, tool details only "load" when the model considers that tool (token efficiency), and you update one tool description instead of a paragraph buried in a 5000-token prompt. → SYSTEM_PROMPT.md
Structured output: when and how
Add an outputParserStructured sub-node (wired ai_outputParser) when downstream needs strict JSON, not free-form text. Two rules:
Use
schemaType: 'manual'with a real JSON Schema, notjsonSchemaExample. An example can't express required-vs-optional, enums, numeric ranges, or array constraints — you outgrow it the first time the shape gets non-trivial. Reach forfromJson+ an example only for throwaway shapes.autoFix: truewith a coding-capable fixer model. Wire a second model into the parser'sai_languageModelslot. Reconciling broken JSON against a schema is a coding task — a weak fixer just produces another malformed retry and burns tokens.
→ STRUCTURED_OUTPUT.md for the schema patterns, the load-bearing "DO NOT wrap in markdown" retry line, and the parse-failure cookbook.
Memory: brief mental model
Memory is a sub-node (ai_memory). Without it, every call is stateless — correct for one-shot tasks (classify, summarize). With it, the agent holds a conversation, keyed by whatever expression you bind to sessionKey.
memoryBufferWindow— keeps the last N exchanges per key and persists across executions via n8n's store. The default for chat.contextWindowLengthdefaults to 5, which is very low — 50 is a saner starting point. Messages past the window are gone entirely.memoryPostgresChat/memoryRedisChat— only when memory must be read outside the agent (your own UI, analytics, cross-system). Not needed just to survive restarts; BufferWindow already does that.
Plumb a stable key from the trigger to memory consistently. Chat triggers fill sessionId automatically; for other surfaces derive one (Slack thread_ts, a webhook conversation ID). Never hardcode sessionId: 'default' and never put sessionId behind $fromAI (the model will fabricate a UUID). → MEMORY.md
Binary and the agent boundary
This is the seam that trips people up:
The model CAN see uploaded images (vision) via
options.passthroughBinaryImages: trueon the agent.Tools CANNOT receive binary.
$fromAI()is JSON-only — no base64, no bytes, even through non-AI bindings.The agent's output is text-shaped (or structured-text with a parser). When a model returns image/audio/video bytes, the Agent doesn't surface them at all — there's nothing to recover downstream.
Workaround: pre-stage uploads to storage before the agent runs, inject the storage keys into the system prompt, and let tools accept the key as a string parameter and re-fetch internally. For one-shot media generation, skip the agent and call the provider's native single-call node directly.
The binary mechanics (which storage, how to stage, how to re-fetch) are owned by n8n-binary-and-data — see its agent-tool binary reference. This skill only marks the boundary; don't re-derive the mechanics here.
Human review (gate destructive tools)
When a tool's effect needs human sign-off before execution (sends, payments, refunds, account changes), wrap it with a review tool node — slackHitlTool, discordHitlTool, telegramHitlTool, gmailHitlTool, etc. (n8n names these "Hitl" / human-in-the-loop). The review node sits between the wrapped tool and the agent on the ai_tool connection: wrapped tool → review node → Agent.
Whether sign-off is needed is a product/policy call — surface the question to the user, recommend based on blast radius, and let them decide.
The critical rule: show the actual parameters the wrapped tool will receive. Use the literal {{ $tool.parameters.<name> }} in the approval message, never a $fromAI() paraphrase — otherwise the human approves text the model made up, not the call about to fire. → HUMAN_REVIEW.md
Chat agents (Slack, Discord, Teams, Telegram)
The one non-negotiable, regardless of complexity: any chat-triggered workflow that posts a reply MUST filter out the bot's own user ID, or its own replies re-trigger it in an infinite loop that burns runs and tokens. Prefer trigger-level filtering when available (Slack Trigger's options.userIds is an exclusion list — put the bot ID there); otherwise filter $json.user !== '<BOT_USER_ID>' in the first node after the trigger.
Beyond the filter, a simple bot (trigger → agent → reply) lives fine in one workflow. Split into shell + core + sub-agents only once you need loading UX, sub-agents, multi-surface reuse, or robust error handling:
Shell — trigger, anti-loop filter, event-type Switch, loading/error UX, renders the reply. No LLM.
Core — stateless agent,
chatInput+threadIdinputs, memory keyed onthreadId, tools and sub-agents.Sub-agents — one narrow domain each, called via
.toolWorkflow, stateless (full context inchatInput).
→ CHAT_AGENT_PATTERNS.md for per-surface semantics, threading-as-session, and the full topology.
Persisted n8n Agents (n8n_manage_agents)
A persisted n8n Agent is a different artifact from the AI Agent node covered above: a standalone assistant record — model, instructions, tools, skills, tasks, memory, channels — stored and versioned by n8n itself, managed through n8n_manage_agents (n8n's instance-level MCP server), not a node inside a workflow's JSON.
| You need to… | Use |
|---|---|
A reasoning step inside a workflow, wired with ai_* sub-nodes | AI Agent node (this skill, above) |
| A standalone assistant with its own lifecycle — draft, validate, publish, versions, channels — independent of any one workflow | Persisted Agent (n8n_manage_agents) |
Prerequisites: N8N_MCP_ACCESS_TOKEN configured (separate from the Public API key) and n8n 2.34+ with the agents module enabled. The token is required for every action, including reference/search — without it, nothing works. Separately, reference and search work for any agent regardless of MCP exposure; every other action needs the target agent exposed to MCP (agents created through this tool are exposed automatically — the exposure gate only matters for agents that already existed before this tool touched them).
Build sequence:
action: "reference"— read the config schema and the exact mutate operations before anything else.action: "discover_assets"— list what the agent can actually be wired to. TakesprojectId(fromn8n_list_catalog({kind: "projects"})) andkind:models(with aprovider),integrations,workflows,subagentsormcpServers. One call per kind.action: "create"—projectId,name,config.action: "mutate"— one resource per call (config.patch,skill.upsert/delete,task.upsert/delete,customTool.upsert/delete), always carrying the latest hash forward. Mind the two names: n8n returns it asconfigHashand expects it back asargs.baseConfigHash.argsare forwarded to n8n verbatim, so a near-miss on any field name comes back asINVALID_ARGS, not a helpful correction — which is why step 1 reads the schema first. A stale hash comes back asSTALE_CONFIG— re-getand retry with the fresh one.action: "validate"— before offering tocallorpublish.action: "publish"— only on the user's explicit request, never proactively.
action: "call" runs the agent with real credentials and real tools — a live execution, not a dry run. A result can carry approvals[] for tool calls that need a human decision; never approve on the user's behalf — surface them and resume only after the user decides.
Custom tools are a third code runtime — don't reuse either of the others. A customTool.upsert body is TypeScript, and the only imports it may use are @n8n/agents and zod. This is not the Code node (JavaScript/Python, returns [{json: …}]) and not the AI-agent Custom Code Tool covered by n8n-code-tool (@n8n/n8n-nodes-langchain.toolCode, returns a string, no $fromAI()). Reaching for the wrong contract is the easy mistake here, because all three are "write code the agent calls". Read the shape from action: "reference" before writing one; a compile failure or an unknown agentId surfaces as AGENT_TOOL_ERROR.
Credential caveat: on n8n 2.36.x the agents runtime rejects azureOpenAiApi and aws credentials (reported as missing: ["credential"]); the response's hint names the accepted types instead.
Testing without leaving debris: name throwaway agents [TEST] … and delete them when you're done — a persisted Agent outlives the conversation that made it, unlike a workflow you can leave inactive.
→ n8n-mcp-tools-expert ## Agents for the tool's full action list and error codes.
RAG (retrieval augmented generation)
n8n ships the LangChain RAG primitives (document loaders, splitters, embeddings, vector stores, retrievers). Two opinions worth stating up front:
Rule out cheaper lookups first. Exact lookups → a database or Data Table query, not RAG. Freshness → a live search tool. A small/structured doc set → give the agent list/fetch tools. Reach for a vector store only when there are too many docs to list and queries are semantic.
Wire the vector store as a retrieval tool (
mode: 'retrieve-as-tool',ai_tool) so the agent decides when retrieval is relevant and can phrase the query itself. Embed query and documents with the same model.
→ RAG.md (intentionally thin — defaults depend on data shape and scale).
Reference files
| File | Read when |
|---|---|
| TOOLS.md | Adding tools, choosing among the four types, writing names/descriptions, $fromAI anatomy |
| SUBWORKFLOW_AS_TOOL.md | Wiring a sub-workflow as a tool via .toolWorkflow, mapping agent-filled vs plumbed params |
| SYSTEM_PROMPT.md | Writing/refactoring a system prompt, the system-prompt-vs-tool-description split |
| STRUCTURED_OUTPUT.md | Forcing JSON output, configuring autoFix, the fixer model, parse-failure fixes |
| MEMORY.md | Choosing a memory type, persistence, sessionId handling |
| HUMAN_REVIEW.md | Adding human approval, approval-message content, multi-channel approver |
| CHAT_AGENT_PATTERNS.md | Building a Slack/Discord/Teams/Telegram bot, shell + core + sub-agents topology |
| RAG.md | Retrieval-augmented agents (thin by design) |
| EXAMPLES.md | Concrete node-object snippets: stateless agent core, Slack router shell, domain sub-agent |
Anti-patterns
| Anti-pattern | What goes wrong | Fix |
|---|---|---|
Generic tool names (tool1, doStuff, runQuery) | Model can't tell which tool to pick — skips them or hallucinates params | Verb-first specific names: Search customer database, Generate image with Veo |
| Empty or one-line tool descriptions | Model has no idea when to invoke; bad selection, no error | Write a real description: what it does, when to use, what each param means |
| Cramming per-tool instructions into the system prompt | Bloated prompt, no reuse, per-tool guidance buried | Move tool-specific instructions into tool descriptions |
| Agent + Switch to route on natural language | Two nodes + prompt boilerplate where Text Classifier is one node | Use Text Classifier — each category gets its own output handle (name and description) |
| Wrapping image/audio/video generation in an Agent | Binary doesn't flow through tools or out of the agent output | Use the provider's native single-call node directly |
outputParserStructured without autoFix | One malformed response halts the workflow | autoFix: true + a coding-capable fixer model |
| Passing binary directly to a tool | Doesn't work — binary can't cross the tool boundary | Pre-stage to storage, pass keys; see n8n-binary-and-data |
Hardcoded sessionId / no sessionId / sessionId behind $fromAI | Conversations cross, or the model fabricates a UUID | Plumb a stable key from the trigger to memory and tools |
| Two near-identical tools | Selection is non-deterministic, model gets confused | One tool with internal branching driven by a parameter |
| Chat bot with no bot-user filter | Its own replies re-trigger it → infinite loop | Exclude the bot user ID at the trigger or first node |
maxIterations left at the low default on a multi-tool agent | "Max iterations reached" / empty output | Raise options.maxIterations |
Filling the human-review message via $fromAI() | Approver signs off on a paraphrase, not the real call | Use literal {{ $tool.parameters.<name> }} |
What's NOT available via the community MCP
| Want to do | Reality |
|---|---|
| Chat-test a workflow's AI Agent node end-to-end interactively | n8n_test_workflow runs the workflow, but a true multi-turn chat session against the node is a UI activity (canvas chat tester). A persisted Agent, by contrast, can be run live via n8n_manage_agents call — see "Persisted n8n Agents" above. |
| Set credentials' actual secret values | n8n_manage_credentials creates/updates credential records, but the agent provider keys themselves are entered/verified in the UI. |
| Assign a workflow's Error Workflow | UI only — see n8n-error-handling. Build the catch-all, then hand the user the UI step. |
| Pin the exact model availability per instance | Model lists shift between versions — search_nodes/get_node reflect what's installed. Verify on the target instance. |
What the MCP can do: search and inspect every LangChain node (search_nodes, get_node), validate node config and the whole graph (validate_node, validate_workflow), build and patch the agent and its sub-nodes (n8n_update_partial_workflow with addConnection on ai_* outputs), test (n8n_test_workflow), and pull the saved JSON to verify wiring (n8n_get_workflow). The deep AI-agent guide also lives in tools_documentation({topic: "ai_agents_guide", depth: "full"}).
Integration with other skills
n8n-workflow-patterns (
ai_agent_workflow.md) — the high-level "agent in a workflow" shape. This skill is the deep dive; start there for architecture.n8n-mcp-tools-expert — node-type formats (short form for
get_node, long form in JSON) and tool-selection guidance. Consult before any MCP call.n8n-node-configuration —
displayOptions-driven fields on the agent and sub-nodes; Slack/Block Kit message shapes (NODE_FAMILY_GOTCHAS.md, Slack section).n8n-expression-syntax —
{{ }},$json.output,$now, and$fromAI/$tool.parametersall rely on correct expression syntax.n8n-code-tool — the Custom Code Tool's runtime contract (string in/out, no
$fromAI). Read it before writing a.toolCode.n8n-subworkflows — the sub-workflow primitive that
.toolWorkflowbuilds on (Execute Workflow Trigger inputs/outputs, naming, search-before-build).n8n-binary-and-data — owns the agent-tool binary boundary mechanics (staging uploads, returning generated files).
n8n-validation-expert — interpreting
validate_workflowresults, including AI-connection issues (a tool wired intomaininstead ofai_toolflags as disconnected).n8n-error-handling —
onError: 'continueErrorOutput'on tool sub-workflows and the agent-core call; error UX on chat shells.n8n-code-javascript / n8n-code-python — for Code-node logic inside a tool sub-workflow (different sandbox from the Code Tool).
Quick reference checklist
Before shipping an agent:
[ ] Right node: Agent for tools/memory/multi-turn; Text Classifier for routing; Information Extractor for fields; native node for media
[ ] Model wired via
ai_languageModel[ ] Every tool has a verb-first specific name AND a real description
[ ]
$fromAI()descriptions are specific (format, range, example); identity/limits/sessionId plumbed deterministically, not via$fromAI[ ] Per-tool guidance lives in tool descriptions, not the system prompt
[ ]
$nowin the system prompt (no hardcoded date)[ ]
maxIterationsraised for multi-tool agents[ ] Memory keyed on a stable
sessionKeyfrom the trigger (not'default', not$fromAI);contextWindowLengthraised from 5[ ] Structured output:
schemaType: 'manual'+autoFix: true+ a coding-capable fixer model[ ] Destructive tools wrapped in human review; approval message uses
$tool.parameters, not$fromAI[ ] Chat bots filter the bot's own user ID (trigger-level or first node)
[ ] Binary: model vision via
passthroughBinaryImages; tools get storage keys, never bytes[ ] Validated with
validate_workflowand verified withn8n_get_workflow(sub-nodes onai_*, notmain)
Remember: an agent is only as good as its tool names, descriptions, and system-prompt discipline. The model can't see your wiring — it sees a system prompt and a list of named, described tools. Design those like an API and most "the agent won't behave" problems disappear.
Files
- ATTRIBUTION.mdAdded by AutoGineer877 B
- CHAT_AGENT_PATTERNS.md12.7 KiB
- EXAMPLES.md19.3 KiB
- HUMAN_REVIEW.md9.2 KiB
- LICENSELicence file added by AutoGineer1.1 KiB
- MEMORY.md7.7 KiB
- RAG.md6.4 KiB
- README.md8.2 KiB
- SKILL.md26.7 KiB
- STRUCTURED_OUTPUT.md6.8 KiB
- SUBWORKFLOW_AS_TOOL.md9.3 KiB
- SYSTEM_PROMPT.md6.8 KiB
- TOOLS.md11.1 KiB
SUBWORKFLOW_AS_TOOL.md
Sub-workflow as agent tool
The default agent-tool shape for anything beyond one node is the Tool Workflow node (@n8n/n8n-nodes-langchain.toolWorkflow). Any sub-workflow becomes a tool the agent calls, with typed inputs filled by $fromAI(). It composes with everything good about n8n: branching, error handling, sub-workflow reuse, native nodes, custom logic.
For the sub-workflow primitive itself (Execute Workflow Trigger inputs/outputs, stateless design, naming, search-before-build), see n8n-subworkflows — this reference only covers the agent-tool angle.
Why this is the default in n8n
In raw LangChain a tool is a function. In n8n a tool can be a whole workflow, so it can:
Branch on input (IF / Switch).
Call multiple APIs and aggregate.
Have its own retries, fallbacks, error handling.
Call other sub-workflows.
Read/write Data Tables.
Be tested independently with
n8n_test_workflowand pinned data.Be reused across agents AND non-agent workflows.
A function-as-tool can't do most of that without growing into a workflow anyway. n8n gives you the workflow primitive directly.
The shape: two halves
1. The sub-workflow side — an Execute Workflow Trigger with typed inputs
{
"parameters": {
"workflowInputs": {
"values": [
{ "name": "imagePrompt", "type": "string" },
{ "name": "imageName", "type": "string" },
{ "name": "sessionId", "type": "string" }
]
}
},
"type": "n8n-nodes-base.executeWorkflowTrigger",
"typeVersion": 1.1,
"name": "When Executed by Another Workflow"
}Each declared input becomes a parameter the caller can fill. The trigger must be in "Define Below" mode (typed fields), not passthrough — passthrough has no schema, so the agent has nothing to fill via $fromAI. Two exceptions: (a) the sub-workflow needs binary (it can't be an agent tool directly — pre-stage to storage and pass storage keys as typed string fields, see n8n-binary-and-data), or (b) the tool takes no inputs at all (passthrough is the only option, and the tool's only decision is whether to invoke).
Type enforcement happens on the agent side via the type argument of $fromAI, not at the trigger. Allowed types: string, number, boolean, json. Match them.
2. The Tool Workflow side — points at the sub-workflow, binds params
{
"parameters": {
"description": "Use to create a new image from a prompt OR edit an existing image. Pass imageName as the storage key (e.g. \"abc123.png\") to edit; leave empty to generate from scratch. Returns { imageUrl, imageKey }.",
"workflowId": { "__rl": true, "value": "<sub-workflow-id>", "mode": "list" },
"workflowInputs": {
"mappingMode": "defineBelow",
"value": {
"imagePrompt": "={{ $fromAI('imagePrompt', 'Detailed prompt describing the desired image', 'string') }}",
"imageName": "={{ $fromAI('imageName', 'Storage key of an existing image to edit, or empty for new generation', 'string') }}",
"sessionId": "={{ $('Chat Trigger').first().json.sessionId }}"
},
"schema": [
{ "id": "imagePrompt", "displayName": "imagePrompt", "type": "string", "display": true },
{ "id": "imageName", "displayName": "imageName", "type": "string", "display": true },
{ "id": "sessionId", "displayName": "sessionId", "type": "string", "display": true }
]
}
},
"type": "@n8n/n8n-nodes-langchain.toolWorkflow",
"typeVersion": 2.2,
"name": "Generate or edit image"
}Wire it into the agent with ai_tool:
"Generate or edit image": {
"ai_tool": [[{ "node": "AI Agent", "type": "ai_tool", "index": 0 }]]
}The mapping is per-input:
Agent-filled:
={{ $fromAI('paramName', 'description', 'string') }}— the agent decides.Plumbed:
={{ $('SourceNode').first().json.field }}— your workflow fills it.
The sessionId line is critical: it is NOT an agent decision. Plumb it from the trigger so memory and session-keyed work stay consistent. Never put sessionId behind $fromAI — the agent will fabricate a UUID.
What the agent sees (and doesn't)
The agent sees the tool's name (the Tool Workflow node's name) and description (a parameter on the node) — both follow the TOOLS.md rules: specific, API-doc style, treated as prompt.
It does not see: the sub-workflow internals, the sub-workflow's own name, or plumbed values like sessionId. Only $fromAI parameters appear in the tool schema. So you can refactor the sub-workflow heavily without changing what the agent sees.
Worked example: one tool, two modes
Goal: an agent that can generate or edit images. Both share most logic; they differ only in whether they download an existing image first.
[Execute Workflow Trigger: { imagePrompt, imageName, sessionId }]
↓
[Crypto: hash for new filename]
↓
[IF: imageName empty?]
├── empty (generate) → [Gemini: generate] ──┐
└── not empty (edit): │
[S3: Download by imageName] │
↓ │
[Gemini: edit with downloaded binary] ───────┤
↓
[S3: Upload result]
↓
[Set: { imageUrl, imageKey }]The agent picks the mode by what it puts in imageName. Two near-identical tools would have made selection harder — collapse them.
Patterns inside the sub-workflow
Return a stable shape (it's a contract)
The caller receives whatever the last node outputs. Pick a shape and keep it across modes:
{ "imageUrl": "https://...", "imageKey": "abc123.png" }Don't sometimes return { url, key } and other times { result: { url, key } }. The output shape is a contract every caller depends on — agents read it as part of the prompt, deterministic callers wire downstream nodes to specific paths. Drift breaks callers silently.
For calls that fail "expectedly" (search with no results), return a branchable shape:
{ "ok": false, "error": "no_results", "message": "No matches found for query" }When to throw instead: Stop and Error
For unexpected-but-handled errors (auth failure, upstream down, unrecoverable input), use a Stop and Error node with a detailed message. It propagates as a thrown error: agents see a tool error and can retry/switch/report; deterministic callers catch it via onError: 'continueErrorOutput'. Pick this over { ok: false } when the outcome is a true error, not a normal branch. For the full error story (4xx/5xx mapping, retries, error workflows) → n8n-error-handling.
Wire onError: 'continueErrorOutput' on fallible nodes
Inside the sub-workflow, fallible nodes (HTTP, S3, DB) should set onError: 'continueErrorOutput' and route to a clean error response, so both agent and deterministic callers receive a structured error instead of a silent halt.
Treat the input contract as an API and document it
The Execute Workflow Trigger's declared inputs ARE this tool's API. Document them in the sub-workflow's description:
Generates or edits an image.
Inputs:
imagePrompt (string, required): detailed image description.
imageName (string, optional): storage key of existing image to edit. Empty = new generation.
sessionId (string, required): chat session ID, used for storage keying.
Returns:
{ imageUrl, imageKey }Keep tool sub-workflows discoverable
Name them with a standard prefix (Subworkflow: or domain-specific). The Tool Workflow node references them by ID (stable), but humans browse the UI by name.
Testing the sub-workflow independently
A sub-workflow tool can be tested without the agent:
n8n_test_workflow({workflowId, method: "prepare"})— which nodes need pinned data.Build one sample item per node, keyed by node name, each wrapped as
{json: {...}}.n8n_test_workflow({workflowId, method: "pinned", pinData})runs it with that data and waits.Verify the output shape matches what the agent will receive.
The routed methods need N8N_MCP_ACCESS_TOKEN and the workflow's "Available in MCP" setting; the default method: "auto" cannot run a sub-workflow, which has no HTTP trigger.
When NOT to use sub-workflow as tool
Simple one-node wrappers — "call this endpoint and return" is shorter as an HTTP Request Tool.
One-off code-only logic specific to this agent — a few lines of pure JS/Python that exist nowhere else work fine as a Custom Code Tool (
.toolCode, see n8n-code-tool). Decision rule: reusable business logic → sub-workflow; one-off agent-specific transform → Code Tool.Capabilities that already exist as native tool nodes — don't wrap
slackToolin a sub-workflow.
For everything else, sub-workflow as tool is the default.
Cross-references
The four tool types overview → TOOLS.md
How
$fromAIdescriptions affect behavior → TOOLS.md "$fromAI()"The sub-workflow primitive (stateless design, naming, I/O) → n8n-subworkflows
Passing binary into tools → n8n-binary-and-data
The Custom Code Tool exception → n8n-code-tool
Requirements and compatibility
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Needs: Needs an MCP server, Needs an API key, Needs Python, Needs Node.js
Security
No issues found by our static scan· No issues found
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Created by
- Romuald Czlonkowski
- Publisher
- Romuald Czlonkowski (Community)
- Source
- czlonkowski/n8n-skills · skills/n8n-agents · commit e037563
- Licence
- MIT License (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by Romuald Czlonkowski or Romuald Czlonkowski.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download n8n-agents.zip