N8n AI Nodes Official
n8nOfficialPublished by n8n
- Skill
- Tool integration
- Automation and integrations
- Apache-2.0
- No issues found
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
This skill covers the implementation of AI capabilities within n8n workflows using the official LangChain node suite. It provides guidance on choosing between the AI Agent node for multi-turn reasoning and specialized nodes like Text Classifier or Basic LLM Chain for specific, leaner tasks. The documentation emphasizes production patterns such as structured output parsing with auto-fixing, human-in-the-loop review for sensitive actions, and modular tool design to ensure agents remain robust, reusable, and safe.
What it does
- Configures AI Agent nodes for multi-turn reasoning, memory, and tool calling.
- Implements specialized nodes for text classification, summarization, and information extraction.
- Integrates RAG primitives for document retrieval and vector store management.
- Enables human-in-the-loop approval gates for tools with side effects.
- Supports structured output parsing with automated retry and fix logic.
- Facilitates media generation workflows using native provider nodes.
When to use it
- When you need to build an AI agent that performs multi-step reasoning or uses tools.
- When you need to route inputs to specific branches using natural language classification.
- When you require structured JSON output from an LLM for downstream processing.
- When you need to gate sensitive actions like payments or refunds with human approval.
- When you need to share conversation memory across chat channels or workflows.
Advantages
- Native integration with n8n's workflow engine and trigger systems.
- Modular design allows tools and system prompts to be reused across different agents.
- Built-in support for structured output and error-resilient parsing.
- Flexible memory management options ranging from stateless to persistent storage.
- Clear separation between agent reasoning and specialized task-specific nodes.
Usage examples
- “How do I set up an agent that requires human approval before sending a refund?”
- The agent explains the HITL (Human-in-the-loop) pattern, showing how to wrap a tool node with a review node (e.g., slackHitlTool) to intercept execution for approval.
- “Should I use an AI Agent or a Text Classifier for routing support tickets?”
- The agent recommends the Text Classifier node for its efficiency and direct output branching, noting it is purpose-built for routing compared to the more complex Agent node.
Install
Download the skill as a ZIP:
https://autogineer.com/agent-skills/n8n-io/n8n-ai-nodes-official/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip n8n-ai-nodes-official.zip -d .agents/skills
Documentation by n8n
n8n AI nodes
The AI Agent node (@n8n/n8n-nodes-langchain.agent) is a multi-turn LLM driver with sub-nodes for the model, memory, tools, and optional output parser. It lives inside a workflow, which owns its trigger, control flow, and lifecycle.
AI Agent node or first-class n8n Agent?
"Build me an agent" means a first-class n8n Agent (n8n-agents-official) by default. Use the node when the workflow owns the control flow and AI is one bounded step, or when the first-class Agent hits a limit:
<!-- TEMPORARY: revisit each limit below when n8n adds tool access to run data, sessions shared between channels and workflows, or Agents on every instance (Enterprise). -->
| Need | Why the node wins today |
|---|---|
| Trigger or upstream data mapped exactly into a tool parameter | Agent tool expressions only see the model's own arguments. Node tools can use $('Webhook').item.json... |
| One conversation shared between a chat channel and other workflows | Agent channel sessions and workflow sessions never meet |
create_agent isn't in the MCP tool list | Agents aren't enabled on this instance |
When to use the Agent node vs raw chat completion
Decision:
Need tool calls, multi-turn reasoning, or memory? Agent. Also a fine default when you don't want to think about it: standardizing on Agent across the workflow is reasonable and makes the path to upgrade simpler.
Want the lightest possible single-shot text-out call? Basic LLM Chain (
@n8n/n8n-nodes-langchain.chainLlm) with a chat-model sub-node (OpenRouter Chat Model,OpenAI Chat Model,Anthropic Chat Model, etc.). No agent loop, no tool/memory/parser slots, easier to debug. Note: chat-model nodes are sub-nodes, and they don't run standalone. They wire into a chain or agent. Agent works here too if you'd rather standardize.Routing to one of N output branches based on natural-language input (the AI's job is to pick the branch)? Use the Text Classifier node (
@n8n/n8n-nodes-langchain.textClassifier). N output handles, one per category, and downstream paths wire directly into each. Every category needs both a name AND a description (the description is what the model picks against, names alone aren't enough). Setoptions.enableAutoFixing: truefor robustness on edge inputs. Pair with a chat-model sub-node (OpenRouter Chat Model,OpenAI Chat Model, etc.). Don't reach for Agent + Switch for this. Text Classifier is one node and purpose-built.Structured output but no tools? Agent is the easier default with future expansion in mind. Basic LLM Chain also accepts an
outputParserStructuredsub-node and works fine where you want the lighter node.Image / audio / video generation? The native single-call node for that provider when calling them directly (OpenAI Image, Gemini Image, ElevenLabs, etc.). HTTP Request when routing through an aggregator (OpenRouter, Together, etc.), because no native aggregator node exists and the native nodes hardcode their provider's base URL on the media operation. Don't wrap media generation in an Agent, see "Binary and the agent boundary" below.
There are other LangChain "chain" / utility nodes for narrow tasks: Information Extractor (pull structured fields from text), Sentiment Analysis (3-way branch), Summarization Chain, Basic LLM Chain.
Agent is a reasonable default for most LLM steps. Reach for Basic LLM Chain when you specifically want the leaner node for a one-shot text call with no tools, memory, or iteration. Reach for Information Extractor / Sentiment Analysis / Summarization Chain / Text Classifier when one of those purpose-built nodes matches the task exactly.
Non-negotiables
Tool names and descriptions are part of the prompt. The model picks tools by name and description. Vague tool node names like (
doStuff) or weak descriptions ("does things with the data") cause silent failure: the model skips your tool, mis-selects it, or hallucinates parameters. Treat both like API design. Seereferences/TOOLS.md.Structured output: parse AND autoFix.
outputParserStructuredwithautoFix: trueand a coding-capable fixer model (e.g., Claude Sonnet 4.6) is the production pattern.
Strong defaults
Tool descriptions are modular prompt fragments. Anything specific to how to call this tool belongs in the tool's description, not the system prompt. Keeps the system prompt focused, and tools become reusable across agents. See
references/SYSTEM_PROMPT.md.Sub-workflow tools (
toolWorkflow) for anything multi-step. Any workflow becomes a tool with typedfromAi()inputs, and composes with branching, error handling, sub-workflows. Seereferences/SUBWORKFLOW_AS_TOOL.md.Wrap tools with user-visible side effects in human review. Sends, payments, refunds, account changes. Gate them behind a Slack / Chat / Discord / Telegram approval node so a human signs off before the tool runs. See
references/HUMAN_REVIEW.md.
The sub-node pattern
The Agent node has a main input (the prompt or user message) and sub-node inputs:
const aiAgent = node({
type: '@n8n/n8n-nodes-langchain.agent',
config: {
name: 'Customer Support Agent',
parameters: {
promptType: 'define',
text: '={{ $json.userMessage }}',
options: {
systemMessage: '...',
passthroughBinaryImages: true, // for vision / multimodal
},
},
subnodes: {
model: openRouterModel,
memory: simpleMemory,
tools: [generateImage, editImage, searchKnowledgeBase],
outputParser: structuredParser, // optional
},
},
})The four sub-node slots:
model(required): the language model. OpenAI, Anthropic, OpenRouter, etc. Use chat-model variants, not completion variants.memory(optional): conversation memory. Without it, every call is stateless. Seereferences/MEMORY.md.tools(optional, but the point of using an agent): tools the agent can call. Seereferences/TOOLS.md.outputParser(optional): forces structured JSON output. Seereferences/STRUCTURED_OUTPUT.md.
Triggers
Different triggers shape the input differently:
Chat Trigger (
@n8n/n8n-nodes-langchain.chatTrigger) withavailableInChat: true: powers the canvas chat tester so you can poke at an agent while building it. Input is{ chatInput, sessionId, files[] }.sessionIdis what memory keys on, so pass it through wherever conversation continuity is needed. Files come in viafiles[], see binary section below. Not a production surface, use Slack / Discord / Teams / Telegram / webhook for that.Webhook: arbitrary input shape, no session by default. Manage continuity by passing a session/conversation ID through the request body and forwarding it to the memory node.
External chat surface (Slack, Discord, Teams, Telegram): every chat-triggered workflow that posts replies MUST filter out the bot's own user ID, or it loops forever potentially crashing n8n. Prefer trigger-level filtering when the surface supports it (Slack's
options.userIdsis an exclusion list); otherwise filter in the first node after the trigger. Semantics differ per surface, seereferences/CHAT_SURFACE_PATTERNS.md. A first-class n8n Agent can connect to Slack, Telegram, Discord, and Linear natively with no workflow, seen8n-agents-official. Beyond the anti-loop filter, a simple bot (trigger → agent → reply) is fine in one workflow. Split into a "shell" workflow + agent-core sub-workflow once you need loading UX, sub-agents, reuse across surfaces, or robust error handling.Manual / Schedule: ad-hoc invocations. Memory rarely useful unless explicitly continuing a previous run.
Execute Workflow Trigger (sub-workflow): when an agent is itself a tool of another agent. Treat the trigger's declared inputs as the contract.
Binary and the agent boundary
The model can see uploaded files (vision) via passthroughBinaryImages: true. But tools cannot receive binary, and fromAi() parameters are JSON only. Base64 is also not accepted by tools, even through non-AI bindings.
Workaround: pre-stage uploads to storage before the agent runs, inject the storage keys into the system prompt, tools accept the key as a string parameter and re-fetch internally. Full pattern in n8n-binary-and-data-official references/AGENT_TOOL_BINARY.md.
On the output side: the Agent's output formatter is text-shaped (or structured-text when an outputParser is wired). When a model returns binary (image bytes, audio bytes, video), the Agent doesn't surface it at all. There's nothing to dig out downstream, and trying to recover it via a Code or Set node after the Agent does not work. For one-shot media generation, use the provider's native single-call node directly such as @n8n/n8n-nodes-langchain.googleGemini or @n8n/n8n-nodes-langchain.openAi.
The exception: when a media step genuinely belongs in an agent (one tool among several, picked based on conversation context, or editing a previously-generated image), the workaround is a tool sub-workflow that uploads the result to storage and returns a key or URL. Pattern in n8n-binary-and-data-official references/AGENT_TOOL_BINARY.md. Don't reach for this by default. The upload + key + re-fetch path adds nodes and a storage dependency you don't otherwise need. Only when the orchestration actually requires an agent's tool selection.
What goes in the system prompt vs the tool description
| Belongs in system prompt | Belongs in the tool's description |
|---|---|
| Persona, role, voice | What this specific tool does |
| Output format rules ("respond in markdown") | When to use this tool vs others |
| Refusal/safety behavior | What each parameter means and its expected shape |
Display protocols ("show images via ![]() markdown") | Examples of good vs bad invocations |
| Universal context (current date, user role) | Tool-specific gotchas (rate limits, edge cases) |
| Inter-tool flow ("after generating, always show via display protocol") | Tool-specific input transformations |
Benefit: tools become reusable. A well-described tool works in any agent that drops it in. The system prompt stays focused on role and shared behavior.
For deeper guidance and worked examples, see references/SYSTEM_PROMPT.md and references/TOOLS.md.
Tool selection: the four types
Pick the lightest option that covers the job:
Native n8n tool node exists? (e.g.,
slackTool,gmailTool,calculatorTool) Use it. Lowest config overhead.Native node is missing an operation or needs custom params (e.g., a Notion endpoint the node doesn't expose, a non-standard header, a different pagination shape)? HTTP Request Tool with the service's "Predefined Credential Type". Reuses the existing OAuth / API-key credential, gives full API access, no custom auth code.
Multi-step logic, or reusing a sub-workflow already in the project? Sub-workflow as tool (
toolWorkflow). Anything you can build as a workflow becomes a tool with typedfromAi()inputs. The most powerful option in n8n, so default here when in doubt. Seereferences/SUBWORKFLOW_AS_TOOL.md.Calling an external HTTP API the agent should orchestrate directly? HTTP Request Tool. Also good for slow async work via long-poll callback.
Tool already exists as a published, MCP-accessible workflow? MCP tool. Useful for cross-workflow agent capabilities. See
n8n-extending-mcp-official.
See references/TOOLS.md for deeper guidance on each option and how to wire fromAi() parameters.
Human review
Before adding or skipping human review on a tool, check with the user. Whether sign-off is needed is a product / policy call (blast radius, audit requirements, how much they trust the model) that the user is better positioned to make than you. Surface the question, recommend based on the criteria below, and let them decide.
When a tool's effects need human approval before execution (sends, payments, refunds, account changes, customer-facing actions), wrap it with a review tool node: slackHitlTool, discordHitlTool, telegramHitlTool, gmailHitlTool, etc. (n8n's node names use Hitl for the human-in-the-loop pattern, and "human review" is what it's called in the UI.) The review node sits between the wrapped tool and the agent on the ai_tool connection: the wrapped tool's ai_tool output wires into the review node, and the review node's ai_tool output wires into the Agent. The agent calls through, the review node pauses for approval, on approve, the wrapped tool runs.
Default to / recommend human review when:
The tool sends, pays, refunds, or otherwise mutates user-visible state.
The approver is different from the chatter (manager-approves-customer-action, support team approves a customer-triggered refund).
The trigger is non-interactive (order, form, schedule) but the tool's effect needs human sign-off.
Approval messages should display the actual parameters the wrapped tool will receive, not text the model paraphrases. Use $tool.parameters.<name> directly, or iterate over $tool.parameters to list every param. Don't fill the approval text via fromAi(). You'd be approving a paraphrase, not the literal call. Customize button labels with the actual values, e.g. Approve {{ $tool.parameters.amount }} refund.
Full config patterns, per-platform setup, and the multi-channel approver pattern in references/HUMAN_REVIEW.md.
Output parsing: when and how
Add an outputParser sub-node when downstream needs structured data, not free-form text.
const parser = outputParser({
type: '@n8n/n8n-nodes-langchain.outputParserStructured',
config: {
parameters: {
schemaType: 'manual',
inputSchema: JSON.stringify({
type: 'object',
properties: {
score: { type: 'integer', minimum: 1, maximum: 5 },
category: { type: 'string', enum: ['bug', 'feature', 'question'] },
reason: { type: 'string' },
tags: { type: 'array', items: { type: 'string' } },
},
required: ['score', 'category', 'reason'],
}),
autoFix: true,
customizeRetryPrompt: true,
prompt: '...retry instructions...', // generally leave as default
},
subnodes: {
languageModel: fixerModel, // coding-capable model, e.g. Claude Sonnet 4.6
},
},
})Use
schemaType: 'manual'with a real JSON schema, notjsonSchemaExample. An example can't express optional fields, enums, value ranges, or array constraints, so you outgrow it the first time the shape gets non-trivial. A schema lets you mark fields required vs optional, define enums, constrain numbers and string formats, and gives the model clearer rules to follow. Reach forschemaType: 'fromJson'with an example only for one-off shapes you're certain will never grow constraints.autoFix: trueadds retry on parse failure. Wire a coding-capable model as the fixer sub-node (e.g., Claude Sonnet 4.6 or similar). Fixing malformed JSON against a schema is a structured-output / coding task, and a weak or generic model often produces another malformed retry, defeating the point.
For the full pattern including custom retry prompts, see references/STRUCTURED_OUTPUT.md.
Memory: brief mental model
No memory: stateless. Right for one-shot tasks (classify, summarize).
memoryBufferWindow: keeps the last N messages per memory key and persists across executions via n8n's internal store. The key is whatever expression you bind tosessionKey. Chat triggers fillsessionIdautomatically, but you can key on anything (Slackthread_ts, a webhook conversation ID, a multi-tenant composite). The default for chat memory. The "window" is the sliding cap on how many messages stay in context, not a scope on persistence.memoryPostgres/memoryRedis/ similar: reach for these when you need to query or read memory outside the agent: displaying conversation history in your own UI, analytics on past chats, or sharing memory with another system. OtherwisememoryBufferWindowis enough.
Plumb a stable key from the trigger to memory consistently, or conversations get crossed. See references/MEMORY.md.
RAG (retrieval augmented generation)
n8n has the LangChain RAG primitives: document loaders, text splitters, embeddings, vector stores, retrievers, rerankers. The pieces work, but opinionated end-to-end recipes ("which vector store, which chunking, when to rerank") depend heavily on data shape and scale.
This skill keeps RAG opinions thin on purpose. See references/RAG.md for more details on RAG.
Reference files
| File | Read when |
|---|---|
references/TOOLS.md | Adding tools to an agent, choosing between the four tool types, writing tool names and descriptions |
references/SUBWORKFLOW_AS_TOOL.md | Wiring a sub-workflow as an agent tool via toolWorkflow, mapping fromAi overrides |
references/SYSTEM_PROMPT.md | Writing or refactoring a system prompt, deciding what goes in the system prompt vs tool descriptions |
references/STRUCTURED_OUTPUT.md | Forcing JSON output, configuring autoFix retries, validating downstream |
references/MEMORY.md | Choosing a memory type, persistence and sessionId handling |
references/RAG.md | Building retrieval-augmented agents, intentionally a stub |
references/HUMAN_REVIEW.md | Adding human approval to a tool, configuring approval messages, multi-channel approver patterns |
references/CHAT_SURFACE_PATTERNS.md | Building a chat bot workflow on Slack, Discord, Teams, Telegram, or any custom chat surface: shell + core + sub-agents, including a first-class Agent as the core via Message an Agent |
Anti-patterns
| Anti-pattern | What goes wrong | Fix |
|---|---|---|
Generic tool names (doStuff, runQuery) | Model can't tell which tool to pick, skips them or hallucinates parameters | Verb-first specific names: Search customer database, Generate image with Veo |
| Empty or one-line tool descriptions | Model has no clue when to invoke, bad selection | Write a real description: what it does, when to use, parameter meanings |
| Cramming everything into the system prompt | Bloated prompt, reuse impossible, per-tool guidance buried | Move tool-specific instructions to tool descriptions, keep system prompt to persona + global rules |
| Code-node tool where a sub-workflow would work | Not reusable, can't be tested independently, can't compose with branching | Use toolWorkflow with a proper sub-workflow |
| Passing binary directly to a tool | Doesn't work, binary can't cross the tool boundary | Pre-stage to storage, pass keys via fromAi, tool fetches internally. See n8n-binary-and-data-official |
outputParserStructured without autoFix | One bad model output and the workflow fails | Set autoFix: true with a cheap fixer model |
Hardcoded sessionId or no sessionId | Conversations cross or memory never matches | Pass sessionId from trigger consistently to memory and tools |
| Two near-identical tools instead of one with branching | Model gets confused, selection is non-deterministic | One tool with internal branching driven by parameters |
| Hardcoding a system prompt that's reused across workflows or iterated often | Editing requires republishing, can't share across workflows, tuning churn lives in node JSON | Store in a Data Table, load at runtime |
| Wrapping image / audio / video generation in an Agent | Binary doesn't flow through tools or out of the output formatter, Agent adds nodes for no gain | Use the provider's native single-call node directly (OpenAI Image, Gemini Image, ElevenLabs), HTTP Request only when going through an aggregator |
| Reaching for Agent + Switch to route on natural-language input | Two nodes plus prompt boilerplate where Text Classifier is one node with N built-in output branches | Use Text Classifier (@n8n/n8n-nodes-langchain.textClassifier), each category gets its own output handle, wire downstream paths directly |
| Tool that mutates user-visible state (send, pay, refund) without human review | Agent fires irreversible action on a wrong inference | Wrap with the review tool node that fits the channel (Slack/Chat/Discord/Telegram), show actual params via $tool.parameters |
Filling the review approval message via fromAi() | The model paraphrases, you approve text not values | Use $tool.parameters.<name> directly so the literal call is visible |
| Chat-triggered agent workflow that posts replies without filtering out the bot's own user ID | Bot's own messages re-trigger the workflow, infinite loop that burns runs and tokens until rate limits or n8n concurrency stops it | Prefer trigger-level filtering when available (Slack Trigger's options.userIds is an exclusion list, put the bot ID there). Otherwise filter on $json.user !== '<BOT_USER_ID>' (or the surface equivalent) as the first node after the trigger. Required for ANY chat-triggered workflow that sends a reply (Slack, Discord, Teams, Telegram), regardless of complexity. See references/CHAT_SURFACE_PATTERNS.md for per-surface semantics |
Passing the bare blocks array to the Slack node's blocksUi when the agent returns Block Kit | The Slack node accepts the input silently and posts the message with no rich content; no error, no warning | Wrap as { "blocks": [...] } with the value as a real array, not a stringified one. Expression: ={{ { "blocks": $('Agent').item.json.output.blocks } }}. See n8n-node-configuration-official references/COMMS_NODES.md "Block Kit messages" |
Files
- ATTRIBUTION.mdAdded by AutoGineer848 B
- LICENSELicence file added by AutoGineer11.0 KiB
- SKILL.md22.4 KiB
- references/CHAT_SURFACE_PATTERNS.md19.9 KiB
- references/HUMAN_REVIEW.md11.4 KiB
- references/MEMORY.md9.5 KiB
- references/RAG.md8.1 KiB
- references/STRUCTURED_OUTPUT.md6.7 KiB
- references/SUBWORKFLOW_AS_TOOL.md12.0 KiB
- references/SYSTEM_PROMPT.md7.4 KiB
- references/TOOLS.md12.0 KiB
- references/examples/agent-core.json13.1 KiB
- references/examples/notion-ideas-subagent.json7.8 KiB
- references/examples/slack-router.json12.3 KiB
references/CHAT_SURFACE_PATTERNS.md
Chat surface patterns: shell + core + sub-agents
For external chat surfaces: Slack, Discord, Microsoft Teams, Telegram, embedded webhook chats. The pieces (memory, tools, sub-workflow as tool, structured output) are covered in their own refs. This file covers the multi-workflow composition that production chat agents grow into, plus chat-surface-specific gotchas the other refs don't.
The shell is the same whether the core is an AI Agent node sub-workflow or a first-class n8n Agent called with Message an Agent. For the second, see "Core as Message an Agent" below. If the first-class Agent's own channel (Slack, Telegram, Discord, Linear) covers the need, no shell is needed at all: see n8n-agents-official references/CHANNELS.md.
Anti-loop filtering is required regardless of complexity. Any chat-triggered workflow that posts a reply MUST filter out the bot's own user ID right after the trigger, or it triggers itself forever. That's the minimum bar for every bot, not a reason to split. Some triggers may have built in parameters for this.
Beyond that, a simple bot (one trigger → one agent → one reply, with the bot-user filter) lives fine in a single workflow. The shell + core + sub-agents split is for production robustness, not the default. It earns its keep once any of these is true:
The bot needs loading-state UX (typing indicator, reaction, placeholder message) and graceful error handling beyond a single message.
The bot is invoked from more than one surface (Slack AND Discord, Teams AND Telegram).
There are specialist domains the agent shouldn't carry inline (Notion DB schema, CRM custom fields, Linear labels).
The agent or its tools will be reused across other workflows.
If none of those apply, keep it in one workflow (with the bot-user filter still in place).
The shape:
[chat-surface workflow] ──► [agent core workflow] ──► [sub-agent workflows]
("the shell") ("the brain") ("specialists")
- Trigger from the surface - Stateless - One narrow domain each
- Anti-loop filter - chatInput + threadId - chatInput only
- Routing / event types - Memory keyed on threadId - Their own tools + model
- Loading + error UX - Tools, sub-agents
- Render the replyExamples in examples/slack-router.json, examples/agent-core.json, examples/notion-ideas-subagent.json.
The shell
Receives chat events, decides whether to respond, manages UX, calls the core, renders the reply. No reasoning, no LLM.
Anti-loop filter (load-bearing)
The bot's own messages re-trigger the workflow. Slack, Discord, Teams all emit events for posts the bot itself made. Without filtering, every reply triggers another run, then another, until rate limits or n8n concurrency stops it.
Prefer trigger-level filtering when the trigger supports it. Some triggers expose user-ID filtering in their options so the loop breaks before any downstream node fires. Semantics differ per surface, verify against the version you're on:
Slack (
n8n-nodes-base.slackTrigger):options.userIdsis an exclusion list. Listed users are dropped before the workflow runs. Put the bot's user ID here for anti-loop. Verified in the trigger source: the handler returns earlyif (userIds.includes(event.user)).Telegram (
n8n-nodes-base.telegramTrigger):additionalFields.userIdsis an inclusion / allowlist (only listed users fire the trigger). Not a bot-exclusion filter, but useful for restricting a private bot to specific human users; bot loops are usually a non-issue on Telegram since bots don't see their own messages by default. Pair withadditionalFields.chatIdsfor scope.Discord, Teams: no native user-level trigger filter. The downstream filter node below is the only option.
Slack trigger-level example:
const slackTrigger = trigger({
type: 'n8n-nodes-base.slackTrigger',
config: {
parameters: {
trigger: ['message'],
channelId: { __rl: true, mode: 'list', value: '<CHANNEL_ID>' },
options: {
userIds: '={{ ["<BOT_USER_ID>"] }}',
},
},
},
})channelId (getChannels) and the exclusion options.userIds (getUsers) are lookup values: resolve real IDs via explore_node_resources instead of hardcoding the placeholders. RLC values in this skill's examples/*.json are instance-specific, re-resolve before reuse.
When the trigger filters at its own boundary, you don't need a separate filter node.
When the trigger doesn't expose a usable exclusion filter (currently Teams, plus Discord via community nodes that vary), the first node after the trigger must filter the bot's own user ID out:
const filterBot = node({
type: 'n8n-nodes-base.filter',
config: {
parameters: {
conditions: {
conditions: [
{
leftValue: '={{ $json.user }}',
rightValue: '<BOT_USER_ID>',
operator: { type: 'string', operation: 'notEquals' },
},
],
},
},
},
})The bot user ID is the API ID from your bot's auth (Slack bot_user_id, Discord application ID, Teams botId).
Switch on event type and identity
The same trigger fires for messages, reactions, threads, mentions, slash commands, button clicks. One Switch right after the bot-loop filter routes each event to the right handler. The shell stays thin: it classifies the event and dispatches, and the actual work happens in dedicated sub-workflows per case.
Example for a Slack assistant (your shape will vary by what you support):
Switch case Goes to
──────────────────────────────────────────────────────────────────────────
"owner message" ──► Execute Workflow: agent-core
"owner reaction" ──► no-op (or Execute Workflow: reaction-handler)
"unknown user" ──► Send canned reply (or 4xx-shaped equivalent)
"slash command: /summary" ──► Execute Workflow: summary-command
"slash command: /search" ──► Execute Workflow: search-command
"button click" ──► Execute Workflow: interaction-handlerEach case has its own sub-workflow because the routing decision (what kind of event is this) and the work (run an agent, run a one-off tool, post a follow-up) are different concerns and often need different models, timeouts, or memory shapes. Adding a new slash command or button means adding one Switch output and one sub-workflow, not a new top-level workflow with its own trigger.
Slack-specific routing notes (payload shapes evolve on both Slack's side and n8n's, so verify field paths against a live event before relying on the exact shape):
Reactions and mentions typically flow through the Slack Trigger as Events API events (
reaction_added,app_mention).Slash commands and Block Kit button clicks generally don't come through the Slack Trigger, since Slack delivers those to separate Request URLs (Slash Command Request URL and Interactivity Request URL) outside the Events API. Two ways to bring them into the shell:
Add a Webhook node as a second trigger in the same shell, configured as Slack's Request URL for that case. The shell then has Slack Trigger + Webhook(s) feeding the same Switch.
Use a community Socket Mode node (e.g.,
@mbakgun/n8n-nodes-slack-socket-mode) that consolidates messages, slash commands, and interactivity into a single trigger.
For matching, slash commands generally expose a
commandfield, and Block Kit interactions arrive withtype === 'block_actions'and anactionsarray (commonly routed byactions[0].action_id). These are reasonable starting points, but check the captured payload in your version before hardcoding.
Loading-state UX
Users assume nothing is happening if they don't see acknowledgement. Pattern: add a loading indicator before the agent call, remove it on every exit path, including error.
[Trigger] → [Filter bot] → [Switch]
→ (owner message)
→ [Add loading reaction] # :chatgpt:, :spinner:, etc.
→ [Execute Workflow: Agent core]
onError: 'continueErrorOutput'
├── (success) → [Remove reaction] → [Send reply]
└── (error) → [Remove reaction] → [Send error message with link]The error path is the easy one to forget. Without it, the loading indicator sits forever and the user thinks the bot is still working. onError: 'continueErrorOutput' on the Execute Workflow node enables the second branch (see n8n-error-handling-official references/NODE_ERROR_OUTPUTS.md).
For Slack: slack node with resource: 'reaction', operation: 'add' and 'remove'. For Discord/Telegram, typing indicators are time-bounded, so for long agents, you could, for instance, send a placeholder message and edit it instead.
Threading as session continuity
Use the surface's thread primitive as the memory sessionKey:
workflowInputs: {
value: {
chatInput: '={{ $("Filter bot").item.json.text }}',
threadId: '={{ $("Filter bot").item.json.thread_ts || $("Filter bot").item.json.ts }}',
},
}thread_ts || ts is the canonical Slack idiom: replies in a thread carry thread_ts referencing the parent, and the parent itself only has ts. Falling back to ts means the parent message becomes the session key for the thread it starts. Each thread is a fresh conversation, and memory doesn't leak across unrelated threads.
User ID, channel ID, and workspace ID alone are wrong: they cross conversations.
When sending the reply, target the same thread by setting otherOptions.thread_ts.replyValues.thread_ts to the same thread_ts || ts. Without it, replies go to the channel root and the thread context is lost.
Error UX: surface, don't hang
The error branch sends a short message with a link to the failed execution:
There was a workflow error. https://<n8n-host>/workflow/<id>/executions/{{ $execution.id }}$execution.id is the live execution ID at the time the error fires. Parameterize the host if you ship across environments.
The agent core
A sub-workflow with two declared inputs: chatInput (the user's message) and threadId (the surface's thread/session ID). Returns the agent's final output: string, structured object, or surface-specific envelope (Block Kit, adaptive card).
The only chat-specific wiring not covered in MEMORY.md is plumbing threadId straight to sessionKey:
sessionIdType: 'customKey',
sessionKey: '={{ $json.threadId }}',threadId flows trigger → (any pass-through nodes) → memory. Don't put it behind fromAi (covered in SUBWORKFLOW_AS_TOOL.md).
Per-execution context (user identity, attached files) goes in a Set node before the agent and gets templated into the system prompt, pattern in SYSTEM_PROMPT.md "File handling injection" and "Template + variables." Don't add a Set node speculatively for context that isn't varying yet. Inline in systemMessage is fine until reuse is real.
For Slack Block Kit, Discord embeds, or Teams adaptive cards, pair the agent with outputParserStructured (see STRUCTURED_OUTPUT.md). The general guidance to default to schemaType: 'manual' with a real JSON Schema applies even more strongly here: Block Kit and adaptive cards lean heavily on oneOf union types across block kinds, plus enums for fields like style and per-block constraints, none of which jsonSchemaExample can express. Examples will produce confidently-wrong block trees that the surface rejects.
Core as Message an Agent
When the brain is a first-class n8n Agent, the core sub-workflow collapses into one Message an Agent node (n8n-nodes-base.messageAnAgent) in the shell. The shell, anti-loop filter, loading UX, and error UX above stay exactly the same.
const askAgent = node({
type: 'n8n-nodes-base.messageAnAgent',
version: 3.1,
config: {
name: 'Ask support agent',
onError: 'continueErrorOutput',
parameters: {
agentId: { __rl: true, mode: 'id', value: '<AGENT_ID>' },
message: '={{ $("Filter bot").item.json.text }}',
advanced: {
session: { session: {
sessionIdType: 'customKey',
sessionKey: '={{ "support-slack-" + ($("Filter bot").item.json.thread_ts || $("Filter bot").item.json.ts) }}',
} },
},
},
},
})What changes versus an AI Agent node core:
The reply is
$json.text, plusstructuredOutput,toolCalls,usage,finishReason, andsession.{agentId, projectId, sessionId, threadId}.Session keys are project-wide, not per agent. A key belongs to the first agent that uses it. The same agent with the same key from any workflow continues the session; a different agent with that key fails with "Failed to create the agent execution record". Prefix every key with the agent's purpose (
support-slack-<thread_ts>), even if only one agent uses it today. Max 74 characters.
<!-- TEMPORARY: sessions can't be shared between native channel conversations (Slack, Telegram, Discord, Linear, n8n Chat) and Message an Agent calls from workflows. Revisit if n8n adds a way to key into a native session from a workflow, or vice versa. -->
A shell session never meets a native-channel session. If the same agent also has its own Slack channel, those threads and the shell's keys are separate conversations with separate memory.
No approvals. A tool with approval required fails the node with "Suspend/resume is not supported in workflow execution context". Put human review in the shell instead: have the agent return a structured decision, then gate the action with a send-and-wait node.
Draft vs published. Manual and chat-mode executions run the Agent's draft; production executions run the published version.
Full node reference: n8n-agents-official references/MESSAGE_AN_AGENT.md.
Sub-agents (agent as a tool)
A sub-agent is its own workflow with its own Agent node, called from the router agent via toolWorkflow. Use it when:
The domain has a schema or enum set the router shouldn't carry (Notion DB properties, Linear labels, CRM fields).
The domain has 5+ tools that would clutter the router's tool list.
The capability is reused across more than one router agent.
The domain warrants a different (cheaper, faster) model than the router.
The contract is stateless. The router sends the full request in chatInput, with no shared memory and no implicit context. Reinforce in both the tool description (router-side) AND the sub-agent's system prompt (callee-side):
IMPORTANT: This tool is stateless. Send all relevant context in a single message. If you need to create an entry, include ALL required fields upfront.
Without that, the router assumes implicit context and the sub-agent guesses.
For everything else about wiring sub-workflows as tools (the toolWorkflow node, fromAi mapping, plumbed-vs-agent-filled values, return shapes), see SUBWORKFLOW_AS_TOOL.md.
Fresh schema injection
When the domain schema can change at runtime (Notion DB property options evolve, Linear teams add labels), refetch on every sub-agent call instead of hardcoding it in the system prompt:
[Execute Workflow Trigger]
↓
[Notion: Get Database] # fetches the schema
↓
[Agent] # system prompt template includes:
## Database Schema
{{ $('Get a database').first().json.properties.toJsonString() }}One extra API call per invocation, and in exchange the sub-agent never returns "that property doesn't exist" because the prompt is stale. Worth it for low-call-volume chat assistants. For high-volume hot paths, cache the schema in a Data Table with a TTL.
Worked example: a personal Slack assistant
Three workflows demonstrating the full pattern:
| Workflow | Role |
|---|---|
| Slack router | The shell. Bot-loop filter, event-type switch, loading reaction, error UX with execution link. |
| Agent core | Stateless agent. Memory keyed on threadId. Native tools (web search, deep research, calculator) plus two sub-agents. Block Kit output. |
| Notion ideas sub-agent | Specialist for a Notion ideas DB. Fetches schema fresh per call. Cheaper model (Claude Haiku 4.6). |
Cross-references
TOOLS.md: tool naming, descriptions,fromAi().SUBWORKFLOW_AS_TOOL.md: thetoolWorkflowshape and parameter mapping.SYSTEM_PROMPT.md: per-execution context, file injection, prompt storage.STRUCTURED_OUTPUT.md: parser config, autoFix, fixer model selection.MEMORY.md: memory types,sessionKeypersistence options.n8n-error-handling-officialreferences/NODE_ERROR_OUTPUTS.md:onError: 'continueErrorOutput'.n8n-node-configuration-officialreferences/COMMS_NODES.md: Slack node parameter shapes.n8n-binary-and-data-officialreferences/AGENT_TOOL_BINARY.md: receiving uploaded files and returning generated files. Read the "Surface-specific seams" section: every platform's file events and image rendering differ.
Anti-patterns
| Anti-pattern | What goes wrong | Fix |
|---|---|---|
| No bot-user-ID filter at the top of the shell | Bot's own messages re-trigger the workflow, infinite loop | Prefer trigger-level filtering when available (Slack: options.userIds is an exclusion list, put the bot ID there). Otherwise filter on $json.user !== '<BOT_USER_ID>' as the first node after the trigger |
Putting the bot's user ID in Telegram's userIds expecting exclusion semantics | Telegram's userIds is an allowlist, not an exclusion list. Only the bot would fire the trigger, which means no human messages get through. Workflow looks "fixed" until you realize it's been silent for hours | Telegram bots don't see their own messages by default, so anti-loop usually isn't needed. If you need to restrict who can talk to the bot, use userIds as the allowlist of permitted humans, not as a place for the bot's ID |
| Loading indicator added but only removed on the success path | User sees the bot stuck "thinking" forever after any error | onError: 'continueErrorOutput' on the agent call, remove the indicator on both branches |
| Using user ID, channel ID, or workspace ID as the session key | Conversations cross threads inside the same channel | Use the surface's thread primitive (Slack `thread_ts |
| Stuffing trigger + UX + agent + sub-agent tools into one workflow when the agent already has multi-surface, sub-agent, or reuse needs | Can't reuse across surfaces, UX leaks into reasoning, harder to test in isolation | Split into shell + core + sub-agents (but only once one of those needs is real, simple bots stay in one workflow) |
| Sub-agent that reads or writes shared memory | Caller can't reason about behavior, not safely retryable | Sub-agents are stateless, full context goes in chatInput |
| Hardcoding domain schema (Notion properties, Linear labels) in a sub-agent's system prompt | Schema rots, sub-agent picks invalid options weeks later | Re-fetch the schema at the start of the sub-agent and template it into the system prompt at runtime |
| Set-before-agent context injection for static text "in case I want to reuse it" | Extra node, no actual reuse, more places to update | Inline in systemMessage until reuse is real, use Set only for per-execution variability |
| Calling the agent core synchronously without an error UX | Worker fails, user sees nothing, debug requires opening n8n | Error branch surfaces a short message with a link to the failed execution |
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 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
- Published by
- n8n
- Publisher
- n8n (Official)
- Source
- n8n-io/skills · skills/n8n-ai-nodes-official · commit 27b6b9f
- Licence
- Apache License 2.0 (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by n8n or n8n.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download n8n-ai-nodes-official.zip