# 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 . 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:///workflow//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**