Generated banner for N8n AI Nodes Official
Generated logo for N8n AI Nodes Official

N8n AI Nodes Official

n8nOfficialPublished by n8n

  • Skill
  • Tool integration
  • Automation and integrations
  • Apache-2.0
  • No issues found

From the author

Use when building or editing AI inside an n8n workflow: the AI Agent node, Text Classifier, Information Extractor, Sentiment Analysis, Summarization Chain, Basic LLM Chain, embeddings, vector stores, single one-shot LLM calls, or AI media generation (image / audio / video) via the native LangChain provider nodes. Also when a first-class n8n Agent can't do the job: trigger data mapped exactly into tool parameters, or memory shared across workflows. Triggers on any `@n8n/n8n-nodes-langchain.*` node, "AI Agent node", "LLM with tools", "tool calling", "fromAi", "system prompt", "memory window", "structured output", "outputParser", "RAG", "vector store", "embeddings", "classify with AI", "extract fields with LLM", "sentiment analysis", "summarize with LLM", "single LLM call", chat triggers with files, Slack / Discord / Teams / Telegram bot workflows, AI image / video / audio generation, or any multi-turn or one-shot LLM step in a workflow.
— the description by n8n

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

  1. Download the skill as a ZIP:

    https://mp-staging.autogineer.com/agent-skills/n8n-io/n8n-ai-nodes-official/download
  2. Unzip 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). -->

NeedWhy the node wins today
Trigger or upstream data mapped exactly into a tool parameterAgent 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 workflowsAgent channel sessions and workflow sessions never meet
create_agent isn't in the MCP tool listAgents 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). Set options.enableAutoFixing: true for 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 outputParserStructured sub-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

  1. 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. See references/TOOLS.md.

  2. Structured output: parse AND autoFix. outputParserStructured with autoFix: true and 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 typed fromAi() inputs, and composes with branching, error handling, sub-workflows. See references/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. See references/MEMORY.md.

  • tools (optional, but the point of using an agent): tools the agent can call. See references/TOOLS.md.

  • outputParser (optional): forces structured JSON output. See references/STRUCTURED_OUTPUT.md.

Triggers

Different triggers shape the input differently:

  • Chat Trigger (@n8n/n8n-nodes-langchain.chatTrigger) with availableInChat: true: powers the canvas chat tester so you can poke at an agent while building it. Input is { chatInput, sessionId, files[] }. sessionId is what memory keys on, so pass it through wherever conversation continuity is needed. Files come in via files[], 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.userIds is an exclusion list); otherwise filter in the first node after the trigger. Semantics differ per surface, see references/CHAT_SURFACE_PATTERNS.md. A first-class n8n Agent can connect to Slack, Telegram, Discord, and Linear natively with no workflow, see n8n-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 promptBelongs in the tool's description
Persona, role, voiceWhat this specific tool does
Output format rules ("respond in markdown")When to use this tool vs others
Refusal/safety behaviorWhat 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 typed fromAi() inputs. The most powerful option in n8n, so default here when in doubt. See references/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
        },
    },
})
  1. Use schemaType: 'manual' with a real JSON schema, not jsonSchemaExample. 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 for schemaType: 'fromJson' with an example only for one-off shapes you're certain will never grow constraints.

  2. autoFix: true adds 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 to sessionKey. Chat triggers fill sessionId automatically, but you can key on anything (Slack thread_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. Otherwise memoryBufferWindow is 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

FileRead when
references/TOOLS.mdAdding tools to an agent, choosing between the four tool types, writing tool names and descriptions
references/SUBWORKFLOW_AS_TOOL.mdWiring a sub-workflow as an agent tool via toolWorkflow, mapping fromAi overrides
references/SYSTEM_PROMPT.mdWriting or refactoring a system prompt, deciding what goes in the system prompt vs tool descriptions
references/STRUCTURED_OUTPUT.mdForcing JSON output, configuring autoFix retries, validating downstream
references/MEMORY.mdChoosing a memory type, persistence and sessionId handling
references/RAG.mdBuilding retrieval-augmented agents, intentionally a stub
references/HUMAN_REVIEW.mdAdding human approval to a tool, configuring approval messages, multi-channel approver patterns
references/CHAT_SURFACE_PATTERNS.mdBuilding 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-patternWhat goes wrongFix
Generic tool names (doStuff, runQuery)Model can't tell which tool to pick, skips them or hallucinates parametersVerb-first specific names: Search customer database, Generate image with Veo
Empty or one-line tool descriptionsModel has no clue when to invoke, bad selectionWrite a real description: what it does, when to use, parameter meanings
Cramming everything into the system promptBloated prompt, reuse impossible, per-tool guidance buriedMove tool-specific instructions to tool descriptions, keep system prompt to persona + global rules
Code-node tool where a sub-workflow would workNot reusable, can't be tested independently, can't compose with branchingUse toolWorkflow with a proper sub-workflow
Passing binary directly to a toolDoesn't work, binary can't cross the tool boundaryPre-stage to storage, pass keys via fromAi, tool fetches internally. See n8n-binary-and-data-official
outputParserStructured without autoFixOne bad model output and the workflow failsSet autoFix: true with a cheap fixer model
Hardcoded sessionId or no sessionIdConversations cross or memory never matchesPass sessionId from trigger consistently to memory and tools
Two near-identical tools instead of one with branchingModel gets confused, selection is non-deterministicOne tool with internal branching driven by parameters
Hardcoding a system prompt that's reused across workflows or iterated oftenEditing requires republishing, can't share across workflows, tuning churn lives in node JSONStore in a Data Table, load at runtime
Wrapping image / audio / video generation in an AgentBinary doesn't flow through tools or out of the output formatter, Agent adds nodes for no gainUse 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 inputTwo nodes plus prompt boilerplate where Text Classifier is one node with N built-in output branchesUse 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 reviewAgent fires irreversible action on a wrong inferenceWrap 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 valuesUse $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 IDBot's own messages re-trigger the workflow, infinite loop that burns runs and tokens until rate limits or n8n concurrency stops itPrefer 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 KitThe Slack node accepts the input silently and posts the message with no rich content; no error, no warningWrap 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

references/HUMAN_REVIEW.md

Human review for agent tools

Human review gates a tool behind explicit human approval. Without approval, the wrapped tool does not run, regardless of how confident the agent is. The default safety pattern for any agent tool with user-visible side effects.

n8n docs and node names use HITL, human-in-the-loop, and Human Review for the same concept, and the review tool nodes are named accordingly (slackHitlTool, discordHitlTool, etc.).

Topology

The review node sits between the wrapped tool and the agent on the ai_tool connection:

[wrapped tool]  --ai_tool-->  [review node]  --ai_tool-->  [Agent]
  • The agent doesn't know the review node is there. It sees the wrapped tool by the wrapped tool's name, description, and parameter schema. The review node is a transparent intercept on the execution path.

  • When the agent calls the wrapped tool, the review node intercepts: it collects the parameters the agent built, pauses, sends an approval prompt to a human via the configured platform, and only on approval does the wrapped tool actually run with those parameters.

SDK syntax

Wire the wrapped tool into the review tool's subnodes.tools array. The agent registers the review tool in its own subnodes.tools. The SDK auto-resolves both ai_tool connections in the saved workflow JSON.

const refundTool = tool({
  type: 'n8n-nodes-base.stripeTool',
  config: {
    name: 'Refund customer',
    parameters: { resource: 'charge', operation: 'update', /* ... */ },
    credentials: { stripeApi: newCredential('Stripe') },
  },
});

const slackReview = tool({
  type: 'n8n-nodes-base.slackHitlTool',
  version: 2.4,
  config: {
    name: 'Slack approval',
    parameters: { message: '...', user: { __rl: true, mode: 'list', value: '...' }, approvalOptions, options }, // resolve user/channel IDs via explore_node_resources (getUsers/getChannels)
    credentials: { slackApi: newCredential('Slack') },
    subnodes: { tools: [refundTool] },  // wrapped tools go here
  },
});

const agent = node({
  type: '@n8n/n8n-nodes-langchain.agent',
  config: { /* ... */ },
  subnodes: { model, tools: [slackReview] },
});

Don't use .to() to wire the wrapped tool into the review tool: .to() creates a main connection, and validate_workflow flags the wrapped tool as DISCONNECTED_NODE. The wrapped-tool-into-review wiring happens through subnodes.tools only.

Tell the agent the review is there

Because the agent doesn't see the review node, it doesn't know its tool is gated. Models with safety priors hedge on destructive-looking tools (send, delete, refund, charge) by default: they refuse, ask the user for confirmation first, or pick a less-direct option. With human review wrapping the tool, that caution is doubled up: the model self-censors AND a human reviews. Sometimes the model never even gets to the review step because it talked itself out of trying.

If you see the agent over-hedging on the wrapped tool (refusing to use it, asking the user for permission first, picking less-direct options), add a note to the wrapped tool's description that the review exists. Per the modular-prompt principle (see SYSTEM_PROMPT.md), tool-specific behavior belongs with the tool so it travels across agents and stays out of an already-busy system prompt.

Example addition to the tool description:

"This tool is gated by a human review step. Use it freely when relevant. A human will see the exact parameters and approve before anything is sent. Don't ask the user for confirmation first."

Don't pre-emptively add this to every wrapped tool. Many agents already use the tool freely without it. Deploy when the symptom (hedging, refusing, talking itself out of trying) actually shows up. Concrete payoff when it does apply: faster turn-taking (one approval click vs back-and-forth confirmation messages), better tool selection (the model isn't artificially de-prioritizing the gated tool), and the reviewer sees the model's actual decisions rather than its hedging.

When to default to human review

  • Sends, pays, refunds, account changes. Anything user-visible and hard to roll back.

  • The approver is different from the chatter. Customer triggers a workflow that asks support staff to approve the refund. Customer doesn't see the approval, support does.

  • Non-chat triggers. Order received, form submitted, schedule fired. The action is taken on someone's behalf, and a person approves before it runs.

  • Production-bound agent tools where the cost of a wrong call (money, customer trust, reputation) outweighs the cost of a one-step delay.

Skip review when the tool is read-only, idempotent and cheap to undo, or when the deployment is internal and exploratory with mocked services.

Available review tool nodes

NodeWhen to use
n8n-nodes-base.slackHitlToolApprover is on Slack (multi-channel pattern: chatter elsewhere, support staff approves in Slack)
n8n-nodes-base.discordHitlToolApprover is on Discord
n8n-nodes-base.telegramHitlToolApprover is on Telegram
n8n-nodes-base.gmailHitlToolApproval via Gmail
n8n-nodes-base.emailSendHitlToolApproval via generic SMTP email
n8n-nodes-base.googleChatHitlToolApproval in Google Chat
n8n-nodes-base.microsoftOutlookHitlToolApproval via Outlook email

More platforms are added over time. Verify what's available on the target instance with search_nodes({ queries: ['hitl'] }).

Response types

responseType chooses the response shape the human sees:

  • approval: button-based approve / disapprove. Sub-configured via approvalOptions.values.approvalType:

    • 'single' (the default): one button (Approve only). Use when disapproval isn't a meaningful choice. The approver either acts or ignores.

    • 'double': two buttons (Approve / Disapprove). For actions where Disapprove needs to be a loud, recordable choice.

  • freeText (Slack / Discord / Telegram / Gmail / etc.): the human types a free-form response. Useful when the agent is genuinely asking a question and any answer is valid.

  • customForm (every variant): a multi-field form supporting text, dropdown, radio, checkbox, and file inputs. The human fills in the form before the wrapped tool runs with those values. This is the practical answer to "editable parameters": define a form whose fields match the wrapped tool's parameters and the human can override what the agent picked.

A two-button "semantic choice" (e.g., "Schedule for today" / "Schedule for tomorrow") is NOT a separate response type. Use responseType: 'approval' with approvalType: 'double' and customize approveLabel / disapproveLabel.

Wait timeout

options.limitWaitTime (in seconds) bounds how long the workflow pauses for approval before erroring out. Default is 45 minutes. Set it explicitly on production workflows. Without setting it, paused executions can sit indefinitely if approvers don't act, and the queue piles up.

Approval message content

Show the actual parameters the wrapped tool will receive. The model picked them, and the human is approving the literal call.

Reference parameters via $tool.parameters.<name>
The agent wants to refund {{ $tool.parameters.amount }} to {{ $tool.parameters.customerId }}.
Reason: {{ $tool.parameters.reason }}.

$tool.name is the wrapped tool's display name. $tool.parameters is the full object the agent built.

Iterate over all parameters

So that adding a parameter to the wrapped tool doesn't silently leave it out of the approval message:

The agent wants to call {{ $tool.name }}:
{{
  $tool.parameters.keys()
    .map(param => `${param}: ${$tool.parameters[param]}\n`)
    .join('')
}}

Drop this into the message field and any future parameter on the wrapped tool shows up automatically.

Don't fill the approval message via fromAi()

fromAi() asks the model to produce a value, including, if you let it, the approval text itself. That means the human approves a model-paraphrased description rather than the literal parameters about to be sent. Defeats the point of human review.

// WRONG
parameters: {
  message: expr("{{ $fromAI('approvalText', 'describe the action for approval') }}"),
}

// RIGHT
parameters: {
  message: expr("Refund {{ $tool.parameters.amount }} to {{ $tool.parameters.customerId }}?"),
}
Customize button labels with the actual values

Embed values in button text so the click is as clear as possible:

approvalOptions: {
  values: {
    approvalType: 'double',
    approveLabel: expr("Approve ${{ $tool.parameters.amount }} refund"),
    disapproveLabel: 'Cancel',
  }
}

A button that says "Approve $50 refund" is unambiguous. A button that says "Approve" alone is not. Default disapprove label is "Decline" if you don't override it.

slackHitlTool also exposes buttonApprovalStyle and buttonDisapprovalStyle ('primary' | 'secondary') for visual emphasis on Slack's button styling.

Multi-channel pattern: approver isn't the chatter

A common production pattern: customer chats with an agent on a website (or via email, order trigger, form submission), and support staff approves sensitive actions in Slack.

[customer chat / order trigger]
    -> [Agent]
        -> [Slack review tool]  ->  [refund / cancel / escalate tool]

The customer never sees the Slack channel. The Slack review message routes via slackHitlTool.parameters.user, a resource locator with a select parent ('channel' | 'user'). The user branch uses mode: 'list' | 'id' | 'username' plus a value, and the channel branch uses channelId. On approval, the wrapped tool fires, and the agent's response goes back to the customer via the original chat path.

This pattern also works without any chat at all: trigger could be a webhook, schedule, form, or queue. The review tool is the only human-facing surface.

UI quirk: test data autofill

When building a review tool, click "Approve" once on the test execution in the canvas editor. n8n autofills the test data so subsequent runs work without manual input. Easy to miss, and new builders often think the tool is broken because the $tool.parameters.<name> is highlighted red since there is no test data.

Editable parameters: use customForm

For "I want to approve, but at $40 instead of $50" workflows, use responseType: 'customForm' (see Response types above). The human fills in a multi-field form whose values feed the wrapped tool, so they can override what the agent picked. Available on every Hitl variant.

Don't try to build editable approvals on top of the approval response type. The form mode is the supported path.

Note: the form mode is not an ideal UX and is reported feeling like a work around. Keep that in mind. Sometimes it may be better to have the user respond with a chat to decline with changes.

Anti-patterns

Anti-patternWhat goes wrongFix
Tool that mutates user-visible state without human reviewAgent fires irreversible action on a wrong inferenceWrap with the right review tool node
Approval message via fromAi()You approve a paraphrase, not the literal callUse $tool.parameters.<name>
"Approve" button with no context in the labelApprover clicks without seeing what they're approvingEmbed actual values: Approve {{ $tool.parameters.amount }}
Review on a channel the approver doesn't actively monitorTool sits indefinitely, executions pile upPick a channel approvers watch, consider TTL / fallback patterns

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.

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download n8n-ai-nodes-official.zip