data/skills/n8n-agents/EXAMPLES.md
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.
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.
{
"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:
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.ai_languageModel but to different nodes.hasOutputParser: true on the agent activates the ai_outputParser slot.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.
{
"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:
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.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.
{
"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:
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.claude-haiku-4.6) than the router — a focused single-domain agent doesn't need the orchestrator's model.maxIterations: 15 — fine for a focused sub-agent (vs 50 on the broad router).Status|status / Type|select key shape is Notion's Name|type convention; match the live schema..toolWorkflow mapping → SUBWORKFLOW_AS_TOOL.md