Back to N8n Mcp

Examples

data/skills/n8n-agents/EXAMPLES.md

2.63.119.3 KB
Original Source

Examples

Three practical node-object snippets for the shell + core + sub-agent topology. These are community n8n JSON fragments to adapt, not full importable exports — credential IDs, workflow IDs, and channel/bot IDs are placeholders. Build with n8n_update_partial_workflow (addNode + addConnection on the ai_* outputs), then verify with n8n_get_workflow and validate_workflow.

For the architecture these fit into, see CHAT_AGENT_PATTERNS.md.


1. Stateless agent core

A reusable agent sub-workflow: chatInput + threadId in, agent output out. Memory keyed on threadId, native tools, a sub-agent tool, and Block Kit structured output with an autoFix fixer model. This is the "brain" called by the shell.

json
{
  "name": "Chat agent core",
  "nodes": [
    {
      "parameters": {
        "workflowInputs": {
          "values": [{ "name": "chatInput" }, { "name": "threadId" }]
        }
      },
      "type": "n8n-nodes-base.executeWorkflowTrigger",
      "typeVersion": 1.1,
      "position": [-480, -96],
      "id": "core-trigger",
      "name": "When Executed by Another Workflow"
    },
    {
      "parameters": {
        "promptType": "define",
        "text": "={{ $json.chatInput }}",
        "hasOutputParser": true,
        "options": {
          "systemMessage": "=You are a concise, direct assistant. Be a thinking partner, not an answer machine.\n\nCurrent date: {{ $now.format('DDDD') }}\n\n## Output\nYou are replying in Slack using Block Kit. Your entire response must be valid JSON with a 'blocks' array at the root. Bold is *single asterisks*. Links are <https://url|text>. Max 10 blocks.\n\n## Tool usage\nFact-check verifiable claims with the web search tool before answering. Use the idea database manager for anything about content ideas.",
          "maxIterations": 50
        }
      },
      "type": "@n8n/n8n-nodes-langchain.agent",
      "typeVersion": 3.1,
      "position": [-48, -96],
      "id": "core-agent",
      "name": "AI Agent"
    },
    {
      "parameters": { "model": "anthropic/claude-opus-4.6", "options": { "temperature": 0.1 } },
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
      "typeVersion": 1,
      "position": [-288, 192],
      "id": "core-main-llm",
      "name": "Main LLM",
      "credentials": { "openRouterApi": { "id": "REPLACE_OPENROUTER_CRED", "name": "OpenRouter" } }
    },
    {
      "parameters": {
        "sessionIdType": "customKey",
        "sessionKey": "={{ $json.threadId }}",
        "contextWindowLength": 50
      },
      "type": "@n8n/n8n-nodes-langchain.memoryBufferWindow",
      "typeVersion": 1.3,
      "position": [-128, 192],
      "id": "core-memory",
      "name": "Simple Memory"
    },
    {
      "parameters": {
        "descriptionType": "manual",
        "toolDescription": "Search the web fast to fact-check a claim or find a source. Use for verifying anything from training data.",
        "query": "={{ $fromAI('query', 'The search query, phrased to match relevant sources', 'string') }}",
        "options": { "search_depth": "fast" }
      },
      "type": "@tavily/n8n-nodes-tavily.tavilyTool",
      "typeVersion": 1,
      "position": [32, 192],
      "id": "core-web-search",
      "name": "Search the web",
      "credentials": { "tavilyApi": { "id": "REPLACE_TAVILY_CRED", "name": "Tavily" } }
    },
    {
      "parameters": {},
      "type": "@n8n/n8n-nodes-langchain.toolCalculator",
      "typeVersion": 1,
      "position": [192, 192],
      "id": "core-calc",
      "name": "Calculator"
    },
    {
      "parameters": {
        "description": "Manages the content-ideas database. Use for ANY task about content ideas: querying, creating, dedupe-checks.\n\nIMPORTANT: This tool is stateless. Send all relevant context in a single message. If creating, include ALL required fields upfront. Returns the page URL for anything referenced or created.",
        "workflowId": { "__rl": true, "value": "REPLACE_SUBAGENT_WF_ID", "mode": "list", "cachedResultName": "Notion ideas sub-agent" },
        "workflowInputs": {
          "mappingMode": "defineBelow",
          "value": { "chatInput": "={{ $fromAI('chatInput', 'The full request to the ideas database, with all context', 'string') }}" },
          "schema": [
            { "id": "chatInput", "displayName": "chatInput", "type": "string", "display": true, "canBeUsedToMatch": true }
          ]
        }
      },
      "type": "@n8n/n8n-nodes-langchain.toolWorkflow",
      "typeVersion": 2.2,
      "position": [352, 192],
      "id": "core-idea-tool",
      "name": "Idea database manager"
    },
    {
      "parameters": {
        "schemaType": "manual",
        "inputSchema": "{ \"type\": \"object\", \"properties\": { \"text\": { \"type\": \"string\" }, \"blocks\": { \"type\": \"array\", \"items\": { \"oneOf\": [ { \"type\": \"object\", \"properties\": { \"type\": { \"const\": \"header\" }, \"text\": { \"type\": \"object\" } }, \"required\": [\"type\", \"text\"] }, { \"type\": \"object\", \"properties\": { \"type\": { \"const\": \"section\" }, \"text\": { \"type\": \"object\" } }, \"required\": [\"type\", \"text\"] }, { \"type\": \"object\", \"properties\": { \"type\": { \"const\": \"divider\" } }, \"required\": [\"type\"] } ] } } }, \"required\": [\"text\", \"blocks\"] }",
        "autoFix": true
      },
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "typeVersion": 1.3,
      "position": [560, 176],
      "id": "core-parser",
      "name": "Structured Output Parser (Block Kit)"
    },
    {
      "parameters": { "model": "anthropic/claude-sonnet-4.6", "options": { "temperature": 0 } },
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
      "typeVersion": 1,
      "position": [620, 336],
      "id": "core-fixer-llm",
      "name": "Fixer LLM (coding-capable)",
      "credentials": { "openRouterApi": { "id": "REPLACE_OPENROUTER_CRED", "name": "OpenRouter" } }
    }
  ],
  "connections": {
    "When Executed by Another Workflow": { "main": [[{ "node": "AI Agent", "type": "main", "index": 0 }]] },
    "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 the web": { "ai_tool": [[{ "node": "AI Agent", "type": "ai_tool", "index": 0 }]] },
    "Calculator": { "ai_tool": [[{ "node": "AI Agent", "type": "ai_tool", "index": 0 }]] },
    "Idea database manager": { "ai_tool": [[{ "node": "AI Agent", "type": "ai_tool", "index": 0 }]] },
    "Structured Output Parser (Block Kit)": { "ai_outputParser": [[{ "node": "AI Agent", "type": "ai_outputParser", "index": 0 }]] },
    "Fixer LLM (coding-capable)": { "ai_languageModel": [[{ "node": "Structured Output Parser (Block Kit)", "type": "ai_languageModel", "index": 0 }]] }
  }
}

What to notice:

  • Memory keyed on threadId, not on a user/channel ID (those cross conversations). The shell supplies threadId.
  • maxIterations: 50 — raised from the low default because this agent chains several tools per turn.
  • $now.format('DDDD') in the system prompt — no hardcoded date.
  • Two models: the main model on the agent, a separate coding-capable fixer wired into the parser. Both connect via ai_languageModel but to different nodes.
  • hasOutputParser: true on the agent activates the ai_outputParser slot.
  • The sub-agent tool's description repeats "This tool is stateless" — the router can't rely on shared context.

2. Slack router shell

The "shell": trigger, trigger-level anti-loop filter, event-type Switch, loading reaction, the agent-core call with an error branch, and the Block Kit reply envelope. No LLM here.

json
{
  "name": "Slack chat router",
  "nodes": [
    {
      "parameters": {
        "trigger": ["message"],
        "watchWorkspace": true,
        "options": { "userIds": "={{ [\"U00000000BOT\"] }}" }
      },
      "type": "n8n-nodes-base.slackTrigger",
      "typeVersion": 1,
      "position": [-288, 48],
      "id": "shell-trigger",
      "name": "Slack Trigger",
      "credentials": { "slackApi": { "id": "REPLACE_SLACK_CRED", "name": "Slack" } }
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": { "version": 3 },
                "conditions": [{ "leftValue": "={{ $json.user === \"U00000000OWNER\" && $json.type === \"message\" }}", "rightValue": "", "operator": { "type": "boolean", "operation": "true", "singleValue": true } }],
                "combinator": "and"
              },
              "renameOutput": true, "outputKey": "Owner message"
            },
            {
              "conditions": {
                "options": { "version": 3 },
                "conditions": [{ "leftValue": "={{ $json.user !== \"U00000000OWNER\" && $json.type === \"message\" }}", "rightValue": "", "operator": { "type": "boolean", "operation": "true", "singleValue": true } }],
                "combinator": "and"
              },
              "renameOutput": true, "outputKey": "Unknown user"
            }
          ]
        }
      },
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.4,
      "position": [-32, 48],
      "id": "shell-switch",
      "name": "Switch"
    },
    {
      "parameters": {
        "resource": "reaction",
        "channelId": { "__rl": true, "value": "={{ $json.channel }}", "mode": "id" },
        "timestamp": "={{ $json.ts }}",
        "name": "spinner"
      },
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.4,
      "position": [240, -64],
      "id": "shell-add-reaction",
      "name": "Add Loading Reaction",
      "credentials": { "slackApi": { "id": "REPLACE_SLACK_CRED", "name": "Slack" } }
    },
    {
      "parameters": {
        "workflowId": { "__rl": true, "value": "REPLACE_AGENT_CORE_WF_ID", "mode": "list", "cachedResultName": "Chat agent core" },
        "workflowInputs": {
          "mappingMode": "defineBelow",
          "value": {
            "chatInput": "={{ $('Slack Trigger').item.json.text }}",
            "threadId": "={{ $('Slack Trigger').item.json.thread_ts || $('Slack Trigger').item.json.ts }}"
          },
          "schema": [
            { "id": "chatInput", "displayName": "chatInput", "type": "string", "display": true },
            { "id": "threadId", "displayName": "threadId", "type": "string", "display": true }
          ]
        }
      },
      "type": "n8n-nodes-base.executeWorkflow",
      "typeVersion": 1.3,
      "position": [480, -64],
      "id": "shell-call-core",
      "name": "Call Agent core",
      "retryOnFail": true,
      "maxTries": 2,
      "waitBetweenTries": 5000,
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "resource": "reaction",
        "operation": "remove",
        "channelId": { "__rl": true, "value": "={{ $('Switch').item.json.channel }}", "mode": "id" },
        "timestamp": "={{ $('Switch').item.json.ts }}",
        "name": "spinner"
      },
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.4,
      "position": [720, -160],
      "id": "shell-remove-reaction-ok",
      "name": "Remove Loading Reaction (success)",
      "credentials": { "slackApi": { "id": "REPLACE_SLACK_CRED", "name": "Slack" } }
    },
    {
      "parameters": {
        "select": "user",
        "user": { "__rl": true, "value": "={{ $('Slack Trigger').item.json.user }}", "mode": "id" },
        "messageType": "block",
        "blocksUi": "={{ { \"blocks\": $('Call Agent core').item.json.output.blocks } }}",
        "otherOptions": {
          "thread_ts": { "replyValues": { "thread_ts": "={{ $('Slack Trigger').item.json.thread_ts || $('Slack Trigger').item.json.ts }}" } }
        }
      },
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.4,
      "position": [960, -160],
      "id": "shell-send-reply",
      "name": "Send Block Kit reply",
      "credentials": { "slackApi": { "id": "REPLACE_SLACK_CRED", "name": "Slack" } }
    },
    {
      "parameters": {
        "select": "user",
        "user": { "__rl": true, "value": "={{ $('Slack Trigger').item.json.user }}", "mode": "id" },
        "text": "=There was a workflow error. https://<your-n8n-host>/workflow/<this-workflow-id>/executions/{{ $execution.id }}",
        "otherOptions": {
          "thread_ts": { "replyValues": { "thread_ts": "={{ $('Slack Trigger').item.json.thread_ts || $('Slack Trigger').item.json.ts }}" } }
        }
      },
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.4,
      "position": [720, 64],
      "id": "shell-send-error",
      "name": "Send error message with execution link",
      "credentials": { "slackApi": { "id": "REPLACE_SLACK_CRED", "name": "Slack" } }
    }
  ],
  "connections": {
    "Slack Trigger": { "main": [[{ "node": "Switch", "type": "main", "index": 0 }]] },
    "Switch": { "main": [[{ "node": "Add Loading Reaction", "type": "main", "index": 0 }], []] },
    "Add Loading Reaction": { "main": [[{ "node": "Call Agent core", "type": "main", "index": 0 }]] },
    "Call Agent core": {
      "main": [
        [{ "node": "Remove Loading Reaction (success)", "type": "main", "index": 0 }],
        [{ "node": "Send error message with execution link", "type": "main", "index": 0 }]
      ]
    },
    "Remove Loading Reaction (success)": { "main": [[{ "node": "Send Block Kit reply", "type": "main", "index": 0 }]] }
  }
}

What to notice:

  • Anti-loop at the trigger: options.userIds: ["U00000000BOT"] is an exclusion list — the bot's own posts never enter the workflow. No separate filter node needed.
  • Call Agent core has onError: 'continueErrorOutput', so main[1] carries the error branch (→ n8n-error-handling). The loading reaction is removed on the success path; the error branch surfaces a link instead of hanging forever.
  • threadId = thread_ts || ts, plumbed straight to the core (which keys memory on it).
  • blocksUi is the { "blocks": [...] } envelope, not the bare array — the bare array fails silently.

3. Domain sub-agent (Notion ideas)

A specialist sub-agent called via .toolWorkflow from the core. It fetches its DB schema fresh on every call and runs on a cheaper model than the router.

json
{
  "name": "Notion ideas sub-agent",
  "nodes": [
    {
      "parameters": { "workflowInputs": { "values": [{ "name": "chatInput" }] } },
      "type": "n8n-nodes-base.executeWorkflowTrigger",
      "typeVersion": 1.1,
      "position": [-240, 0],
      "id": "sub-trigger",
      "name": "When Executed by Another Workflow"
    },
    {
      "parameters": {
        "resource": "database",
        "databaseId": { "__rl": true, "value": "REPLACE_NOTION_DB_ID", "mode": "id" },
        "simple": false
      },
      "type": "n8n-nodes-base.notion",
      "typeVersion": 2.2,
      "position": [-32, 0],
      "id": "sub-get-db",
      "name": "Get a database",
      "credentials": { "notionApi": { "id": "REPLACE_NOTION_CRED", "name": "Notion" } }
    },
    {
      "parameters": {
        "promptType": "define",
        "text": "={{ $('When Executed by Another Workflow').item.json.chatInput }}",
        "options": {
          "systemMessage": "=You manage a Notion ideas database. Query and create idea entries.\n\n## Database schema (fetched fresh this call)\n{{ $('Get a database').first().json.properties.toJsonString() }}\n\n## Rules\n1. Always respond in chat with the result.\n2. Always return the Notion URL for any page created or referenced.\n3. Select/multi-select values must EXACTLY match an existing schema option.\n4. IMPORTANT: you are stateless. If information is missing, list exactly what's needed and remind the caller to resend the complete request with all details.",
          "maxIterations": 15
        }
      },
      "type": "@n8n/n8n-nodes-langchain.agent",
      "typeVersion": 3.1,
      "position": [208, 0],
      "id": "sub-agent",
      "name": "AI Agent"
    },
    {
      "parameters": { "model": "anthropic/claude-haiku-4.6", "options": { "temperature": 0.1 } },
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
      "typeVersion": 1,
      "position": [112, 256],
      "id": "sub-llm",
      "name": "Sub-agent LLM (cheaper than router)",
      "credentials": { "openRouterApi": { "id": "REPLACE_OPENROUTER_CRED", "name": "OpenRouter" } }
    },
    {
      "parameters": {
        "descriptionType": "manual",
        "toolDescription": "Returns all ideas that are still active (not rejected, cancelled, or started).",
        "resource": "databasePage",
        "operation": "getAll",
        "databaseId": { "__rl": true, "value": "REPLACE_NOTION_DB_ID", "mode": "id" },
        "returnAll": true,
        "filterType": "manual",
        "filters": { "conditions": [{ "key": "Status|status", "condition": "does_not_equal", "statusValue": "Rejected" }] }
      },
      "type": "n8n-nodes-base.notionTool",
      "typeVersion": 2.2,
      "position": [304, 256],
      "id": "sub-get-active",
      "name": "Get active ideas",
      "credentials": { "notionApi": { "id": "REPLACE_NOTION_CRED", "name": "Notion" } }
    },
    {
      "parameters": {
        "descriptionType": "manual",
        "toolDescription": "Creates an idea entry. Always enters as status 'Idea'. Select fields must match schema options exactly.",
        "resource": "databasePage",
        "databaseId": { "__rl": true, "value": "REPLACE_NOTION_DB_ID", "mode": "id" },
        "title": "={{ $fromAI('Title', 'Short title of the idea', 'string') }}",
        "propertiesUi": {
          "propertyValues": [
            { "key": "Status|status", "statusValue": "Idea" },
            { "key": "Type|select", "selectValue": "={{ $fromAI('type', 'Type column; must EXACTLY match a schema option', 'string') }}" }
          ]
        }
      },
      "type": "n8n-nodes-base.notionTool",
      "typeVersion": 2.2,
      "position": [480, 256],
      "id": "sub-create",
      "name": "Create idea",
      "credentials": { "notionApi": { "id": "REPLACE_NOTION_CRED", "name": "Notion" } }
    }
  ],
  "connections": {
    "When Executed by Another Workflow": { "main": [[{ "node": "Get a database", "type": "main", "index": 0 }]] },
    "Get a database": { "main": [[{ "node": "AI Agent", "type": "main", "index": 0 }]] },
    "Sub-agent LLM (cheaper than router)": { "ai_languageModel": [[{ "node": "AI Agent", "type": "ai_languageModel", "index": 0 }]] },
    "Get active ideas": { "ai_tool": [[{ "node": "AI Agent", "type": "ai_tool", "index": 0 }]] },
    "Create idea": { "ai_tool": [[{ "node": "AI Agent", "type": "ai_tool", "index": 0 }]] }
  }
}

What to notice:

  • Fresh schema injection: Get a database runs before the agent (on main), and its properties are templated into the system prompt with .toJsonString(). The sub-agent never operates on a stale schema, so it can't pick a select option that was renamed last week.
  • Cheaper model (claude-haiku-4.6) than the router — a focused single-domain agent doesn't need the orchestrator's model.
  • Stateless contract restated in the system prompt — matching the tool description on the core side.
  • maxIterations: 15 — fine for a focused sub-agent (vs 50 on the broad router).
  • The Status|status / Type|select key shape is Notion's Name|type convention; match the live schema.

Cross-references

  • The topology these fit into → CHAT_AGENT_PATTERNS.md
  • The .toolWorkflow mapping → SUBWORKFLOW_AS_TOOL.md
  • Block Kit schema and autoFix → STRUCTURED_OUTPUT.md
  • Error branch on the core call → n8n-error-handling