{"owner":"czlonkowski","repo":"n8n-mcp","hasSkills":true,"totalSkillsCount":91,"totalTokensCount":79001,"categories":["anthropic-skill","root-instruction","claude-rule","subagent-persona"],"hasMcp":true,"mcpConfig":{"mcpServers":{"n8n-mcp":{"command":"npx","args":["-y","@modelcontextprotocol/server-n8n-mcp"]}}},"found":["data/skills/n8n-binary-and-data/SKILL.md","data/skills/n8n-agents/SKILL.md","data/skills/n8n-code-python/SKILL.md","data/skills/n8n-code-javascript/SKILL.md","data/skills/n8n-mcp-tools-expert/SKILL.md","data/skills/n8n-expression-syntax/SKILL.md","data/skills/n8n-code-tool/SKILL.md","data/skills/n8n-multi-instance/SKILL.md","data/skills/n8n-node-configuration/SKILL.md","data/skills/n8n-subworkflows/SKILL.md","data/skills/n8n-error-handling/SKILL.md","data/skills/n8n-self-hosting/SKILL.md","data/skills/n8n-validation-expert/SKILL.md","data/skills/n8n-workflow-patterns/SKILL.md","data/skills/using-n8n-mcp-skills/SKILL.md","AGENTS.md","CLAUDE.md",".claude/agents/code-reviewer.md",".claude/agents/context-manager.md",".claude/agents/debugger.md",".claude/agents/deployment-engineer.md",".claude/agents/mcp-backend-engineer.md",".claude/agents/n8n-mcp-tester.md",".claude/agents/technical-researcher.md",".claude/agents/test-automator.md","data/skills/n8n-agents/CHAT_AGENT_PATTERNS.md","data/skills/n8n-agents/EXAMPLES.md","data/skills/n8n-agents/HUMAN_REVIEW.md","data/skills/n8n-agents/MEMORY.md","data/skills/n8n-agents/RAG.md","data/skills/n8n-agents/README.md","data/skills/n8n-agents/STRUCTURED_OUTPUT.md","data/skills/n8n-agents/SUBWORKFLOW_AS_TOOL.md","data/skills/n8n-agents/SYSTEM_PROMPT.md","data/skills/n8n-agents/TOOLS.md","data/skills/n8n-binary-and-data/AGENT_TOOL_BINARY.md","data/skills/n8n-binary-and-data/BINARY_BASICS.md","data/skills/n8n-binary-and-data/CDN_REQUIREMENT.md","data/skills/n8n-binary-and-data/MERGE_FOR_CONTEXT.md","data/skills/n8n-binary-and-data/README.md","data/skills/n8n-code-javascript/BUILTIN_FUNCTIONS.md","data/skills/n8n-code-javascript/COMMON_PATTERNS.md","data/skills/n8n-code-javascript/DATA_ACCESS.md","data/skills/n8n-code-javascript/ERROR_PATTERNS.md","data/skills/n8n-code-javascript/README.md","data/skills/n8n-code-python/COMMON_PATTERNS.md","data/skills/n8n-code-python/DATA_ACCESS.md","data/skills/n8n-code-python/ERROR_PATTERNS.md","data/skills/n8n-code-python/README.md","data/skills/n8n-code-python/STANDARD_LIBRARY.md","data/skills/n8n-code-tool/ERROR_PATTERNS.md","data/skills/n8n-code-tool/INPUT_SCHEMA.md","data/skills/n8n-code-tool/README.md","data/skills/n8n-error-handling/API_WORKFLOWS.md","data/skills/n8n-error-handling/ERROR_WORKFLOWS.md","data/skills/n8n-error-handling/NODE_ERROR_OUTPUTS.md","data/skills/n8n-error-handling/README.md","data/skills/n8n-error-handling/RESPONSE_SHAPES.md","data/skills/n8n-expression-syntax/COMMON_MISTAKES.md","data/skills/n8n-expression-syntax/EXAMPLES.md","data/skills/n8n-expression-syntax/README.md","data/skills/n8n-mcp-tools-expert/OPERATIONS_GUIDE.md","data/skills/n8n-mcp-tools-expert/README.md","data/skills/n8n-mcp-tools-expert/SEARCH_GUIDE.md","data/skills/n8n-mcp-tools-expert/VALIDATION_GUIDE.md","data/skills/n8n-mcp-tools-expert/WORKFLOW_GUIDE.md","data/skills/n8n-multi-instance/README.md","data/skills/n8n-node-configuration/DEPENDENCIES.md","data/skills/n8n-node-configuration/NODE_FAMILY_GOTCHAS.md","data/skills/n8n-node-configuration/OPERATION_PATTERNS.md","data/skills/n8n-node-configuration/README.md","data/skills/n8n-self-hosting/CREDENTIAL_OVERWRITES.md","data/skills/n8n-self-hosting/DAY2.md","data/skills/n8n-self-hosting/QUEUE_MODE.md","data/skills/n8n-self-hosting/README.md","data/skills/n8n-self-hosting/SECURITY.md","data/skills/n8n-self-hosting/SINGLE_MODE.md","data/skills/n8n-subworkflows/NAMING_AND_DISCOVERY.md","data/skills/n8n-subworkflows/README.md","data/skills/n8n-subworkflows/SUBWORKFLOW_PATTERNS.md","data/skills/n8n-validation-expert/ERROR_CATALOG.md","data/skills/n8n-validation-expert/FALSE_POSITIVES.md","data/skills/n8n-validation-expert/README.md","data/skills/n8n-validation-expert/REVIEW_CHECKLIST.md","data/skills/n8n-workflow-patterns/README.md","data/skills/n8n-workflow-patterns/ai_agent_workflow.md","data/skills/n8n-workflow-patterns/database_operations.md","data/skills/n8n-workflow-patterns/http_api_integration.md","data/skills/n8n-workflow-patterns/scheduled_tasks.md","data/skills/n8n-workflow-patterns/webhook_processing.md","data/skills/using-n8n-mcp-skills/README.md"],"skills":{"data/skills/n8n-binary-and-data/SKILL.md":"---\nname: n8n-binary-and-data\ndescription: Handle files and binary data in n8n correctly. Use when working with files, images, PDFs, attachments, uploads or downloads, base64, vision/multimodal input, or when an AI agent needs a file as tool input or output — and whenever the user mentions $binary, binaryPropertyName, \"read the PDF\", \"attach the file\", \"send the image\", Merge losing binary, or a CDN for chat images. Covers the $binary vs $json split, reading/writing binary, keeping binary alive across transforms with Merge, the agent-tool binary boundary, and the CDN/URL requirement for chat surfaces.\n---\n\n# n8n Binary and Data\n\nEvery n8n item carries two independent slots: `$json` for structured data and `$binary` for file bytes. They travel side by side through the workflow. File contents — the actual PDF, image, or zip — live in `$binary`, never in `$json`. Get that split wrong and you read an empty field, lose a file mid-flow, or hand an AI agent a tool input it can't use.\n\nThis skill covers where binary lives, how to read and write it, how to keep it from being silently stripped, the hard wall between binary and the AI-agent tool boundary, and why chat surfaces need a URL instead of raw bytes.\n\n---\n\n## The three rules that prevent 90% of binary bugs\n\n1. **File contents are in `$binary`, not `$json`.** After an HTTP download, a \"Read Files\", or an email-attachment trigger, the bytes sit in `$binary.<key>`. `$json` holds metadata at most. Reading `$json.data` for file contents gives you nothing.\n\n2. **Binary cannot cross the AI-agent tool boundary — in either direction.** Tool arguments and tool return values are JSON only. An uploaded image can't be passed into a tool as a file, and a tool can't return raw bytes. Pre-stage to storage and pass a key or URL through JSON instead. See `AGENT_TOOL_BINARY.md`.\n\n3. **Chat surfaces render images by URL, not by `$binary`.** Slack, Discord, Teams, Telegram, embedded webhook chat — none of them read the binary slot. The image has to live somewhere a URL can fetch it. See `CDN_REQUIREMENT.md`.\n\n---\n\n## The two slots\n\nEach item is shaped like this:\n\n```json\n{\n  \"json\": { \"customerId\": 42, \"status\": \"sent\" },\n  \"binary\": {\n    \"invoice\": {\n      \"data\": \"<base64-encoded bytes>\",\n      \"mimeType\": \"application/pdf\",\n      \"fileName\": \"invoice-42.pdf\",\n      \"fileExtension\": \"pdf\"\n    }\n  }\n}\n```\n\nThe key inside `binary` (`invoice` here) is the **binary property name**. Most file-handling nodes have a `binaryPropertyName` parameter that points at it — the producer names the slot, the consumer references it by that name. The default key across most nodes is `data`, so when nothing tells you otherwise, assume `$binary.data`.\n\n`$json` and `$binary` are separate namespaces. An expression like `{{ $binary.invoice.fileName }}` reads file metadata; `{{ $json.customerId }}` reads data. They never mix.\n\nThis split also explains a webhook gotcha: a Webhook trigger receiving `multipart/form-data` puts the uploaded file in `$binary` and the accompanying form fields in `$json.body` — so an uploaded file is not somewhere under `$json` at all. (The `$json.body` nesting for webhooks is **n8n-expression-syntax** territory.)\n\nSee `BINARY_BASICS.md` for the full slot anatomy, mime types, and size limits.\n\n---\n\n## Producing binary\n\nYou rarely build a `$binary` slot by hand — nodes populate it for you:\n\n| Source | How binary appears |\n|---|---|\n| HTTP Request with `responseFormat: \"file\"` | Response body lands in `$binary.data` (or the name you set) |\n| Read/Write Files from Disk | File contents read into `$binary` |\n| Storage downloads (S3, Google Drive, Dropbox, etc.) | Downloaded file in `$binary.<key>` |\n| Email triggers with attachments | Each attachment arrives in `$binary` |\n| Provider AI media nodes (image/audio gen) | Set `options.binaryPropertyOutput` so the bytes land where the next node looks |\n\nFor an HTTP download, the one field that matters is `responseFormat`. Confirm it with `get_node` on `nodes-base.httpRequest` — leaving it as the default JSON/string format is the classic reason a downloaded file ends up as garbled text in `$json` instead of clean bytes in `$binary`.\n\n---\n\n## Reading and writing binary in a Code node\n\nMost workflows never need to crack open the bytes — they just pass binary through to a consumer (email attachment, file upload, Slack file). When you do need the raw bytes, do it in a Code node.\n\n**Read** with `getBinaryDataBuffer` — do not try to base64-decode `$binary.<key>.data` by hand:\n\n```javascript\n// Code node, \"Run Once for Each Item\"\nconst buffer = await this.helpers.getBinaryDataBuffer(0, 'data'); // (itemIndex, propertyName)\nconst text = buffer.toString('utf-8');\nconst length = buffer.length;\n\nreturn [{\n  json: { ...$json, length },\n  binary: $input.item.binary,   // pass the binary through, or it's gone\n}];\n```\n\n**Write** by building the slot yourself — base64 the bytes plus a mime type and file name:\n\n```javascript\nconst text = 'Hello, world!';\nreturn [{\n  json: { ok: true },\n  binary: {\n    report: {\n      data: Buffer.from(text).toString('base64'),\n      mimeType: 'text/plain',\n      fileName: 'report.txt',\n      fileExtension: 'txt',\n    },\n  },\n}];\n```\n\nThe Code-node sandbox, helpers, and execution modes are the domain of **n8n-code-javascript** (and **n8n-code-python**) — use those for the language-level detail. The one binary-specific thing to remember here: a Code node that returns `[{ json: {...} }]` without re-attaching `binary` **silently drops the file**. See `BINARY_BASICS.md`.\n\n---\n\n## Keeping binary alive across transforms\n\nJSON-only nodes — Edit Fields (Set), Code, IF, and others — can drop the `$binary` slot from their output. The workflow validates clean and runs without error; the file just isn't there downstream when the email node goes to attach it.\n\nTwo ways to keep it:\n\n- **Pass-through option on the transforming node.** Edit Fields has `includeOtherFields`; a Code node can return `binary: $input.item.binary` explicitly. Cheapest fix when it's available.\n- **Fan out and Merge by position.** Route the source into both the transform and a bypass branch, then recombine with a Merge in `combineByPosition` mode. The JSON comes from the transform side, the binary survives on the bypass side.\n\n```\n[Source with binary] ─┬─→ [Edit Fields: change JSON] ─┐\n                      │      (binary stripped here)     ├─→ [Merge: combineByPosition] ─→ [Email: attach]\n                      └──────────────────────────────────┘\n                          (bypass — binary passes through untouched)\n```\n\n`combineByPosition` pairs item N from each input, so the field counts must line up. The connection wiring and the alternatives for many-strip-point chains (upload-early, sub-workflow) are in `MERGE_FOR_CONTEXT.md`.\n\n---\n\n## The agent-tool binary boundary\n\nThis is the sharpest edge. An AI Agent talks to its tools (Custom Code Tool, Call n8n Workflow Tool, HTTP Request Tool, MCP tools) over JSON. Binary does not fit through that pipe in either direction. The fix is the same shape both ways: **stage the bytes in storage, pass a key/URL through JSON, fetch on the other side.**\n\n**Inbound — a user uploads a file the agent's tool must operate on:**\n\n1. The chat trigger gives you a `files[]` array. Split it out and upload each file to private storage under a hashed key.\n2. Re-merge that branch before the agent runs (it's a synchronization barrier, not decoration), and set `executeOnce: true` on the agent so N files don't trigger N agent runs.\n3. Inject the keys into the agent's system prompt, listing both the original name (human context) and the storage key (what the tool needs), with an explicit \"use EXACTLY this key\".\n4. The tool receives the key as a string argument and downloads the file from storage itself.\n\n**Outbound — a tool generates a file the agent must return:**\n\n1. The tool sub-workflow generates the binary, uploads it to storage, and returns JSON like `{ \"ok\": true, \"key\": \"...\", \"url\": \"https://...\", \"mimeType\": \"image/png\" }`.\n2. The agent embeds the URL in its reply (or passes the key to another tool).\n\n`passthroughBinaryImages: true` on the agent only changes what the **LLM sees** for vision — it does **not** let tools receive the file, and it's image-only (no PDFs, audio, or video). You still need the upload-and-pass-key pattern for any tool. Full patterns, hash strategy, storage choices, and the long-running-tool variant are in `AGENT_TOOL_BINARY.md`.\n\n> Building the tool itself? See **n8n-code-tool** for the Custom Code Tool contract and **n8n-workflow-patterns** for the AI-Agent-with-tools shape.\n\n---\n\n## The CDN requirement for chat surfaces\n\nWhen a workflow generates an image and the user wants it shown inside a chat message:\n\n- **Binary on the item isn't enough.** The chat client renders messages that reference images by URL (or pushes bytes through the platform's own file-upload API). It never reads `$binary`.\n- **The bytes have to live somewhere a URL can fetch over HTTPS.** Upload to an object store or drive first, then embed the returned URL.\n- **n8n has no built-in CDN.** The user provides the storage.\n\nAsk which storage they already use rather than defaulting to S3 — object storage (S3, R2, GCS, Azure Blob, Backblaze B2, Supabase Storage) and drive-style services (Dropbox, Google Drive, OneDrive, Box) all work and all change the URL shape. Cloudflare R2 is the lowest-friction starting point if they have nothing. For sensitive content, use a signed URL with an expiry rather than a permanently public one. See `CDN_REQUIREMENT.md`.\n\n---\n\n## What's NOT available\n\n- **`$fromAI()` cannot carry binary.** It fills tool parameters with strings, numbers, booleans, and objects — never file bytes. Pass a storage key instead.\n- **Tool arguments and returns are JSON only.** There is no \"binary parameter\" on an agent tool, in or out.\n- **n8n ships no CDN or public file host.** Serving a file over a URL is always something the user's storage does, not n8n.\n- **`getBinaryDataBuffer` is a Code-node helper.** It isn't available in the Custom Code Tool sandbox (see **n8n-code-tool**).\n\n---\n\n## Where Data Tables live\n\nFor persistent tabular storage — reference-counting staged files, tracking which keys are live, dedup — that's the `n8n_manage_datatable` surface, owned by **n8n-mcp-tools-expert**. This skill does not cover Data Tables.\n\n---\n\n## Anti-patterns\n\n| Anti-pattern | What goes wrong | Fix |\n|---|---|---|\n| Reading file contents from `$json` | Bytes live in `$binary`; `$json` is empty or metadata only | Read `$binary.<key>`, or `getBinaryDataBuffer` in a Code node |\n| HTTP download without `responseFormat: \"file\"` | Bytes arrive as mangled text in `$json`, not clean binary | Set `responseFormat: \"file\"` on the HTTP Request node |\n| Code node returns `[{json:{...}}]`, no `binary` | The file is silently dropped downstream | Re-attach `binary: $input.item.binary` in the return |\n| JSON transform (Edit Fields/IF) eats the binary | Email/upload node finds nothing to attach | Pass-through option, or fan out + Merge by position |\n| Passing an uploaded file into a tool via `$fromAI` | `$fromAI` can't carry binary; the tool gets nothing | Pre-stage to storage, inject the key in the system prompt, tool fetches by key |\n| Assuming `passthroughBinaryImages` lets tools see the file | It only affects what the LLM sees, and only for images | Still need the upload-and-pass-key pattern for tools |\n| Tool returns raw binary to the agent | Tool output is JSON; bytes don't survive (and bloat context) | Upload, return `{ key, url }` in JSON |\n| Posting `$binary` to a chat surface and expecting an image | Chat clients render by URL, not raw bytes | Upload to storage/CDN, embed the URL or use the platform file API |\n| Hardcoding base64 in a Code node | Huge workflow JSON, slow, leaky | Reference via `$binary`, or upload and reference by URL |\n\n---\n\n## Reference files\n\n| File | Read when |\n|---|---|\n| `BINARY_BASICS.md` | First time handling binary, or reading/writing the `$binary` slot, mime types, size limits |\n| `AGENT_TOOL_BINARY.md` | An agent tool needs an uploaded file, or produces one — the boundary in either direction |\n| `MERGE_FOR_CONTEXT.md` | Binary disappears after a JSON transform and you need to re-attach it |\n| `CDN_REQUIREMENT.md` | Showing images in a chat surface or anywhere that needs URL-referenced images |\n\n---\n\n## Integration with Other Skills\n\n**n8n-code-javascript / n8n-code-python**: the Code node is where you read/write raw bytes (`getBinaryDataBuffer`, `Buffer.from(...).toString('base64')`). Those skills own the sandbox, helpers, and execution-mode detail — this skill owns the rule that binary must be re-attached on return.\n\n**n8n-code-tool**: the Custom Code Tool sandbox is narrower — no `$binary`, no `getBinaryDataBuffer`, no `$fromAI`. When a tool needs a file, this skill's storage-key pattern is how it gets one.\n\n**n8n-workflow-patterns**: the agent-tool binary boundary sits inside the AI-Agent-with-tools pattern; the CDN flow is a generate → upload → reply chain.\n\n**n8n-node-configuration**: `responseFormat`, `binaryPropertyName`, `includeOtherFields`, `binaryPropertyOutput` are all conditional fields — use `get_node` to confirm the exact names on the user's version.\n\n**n8n-expression-syntax**: addressing `$binary.<key>.fileName` vs `$json.body` (webhook uploads in particular) is expression territory.\n\n**n8n-validation-expert**: a dropped binary slot is a silent failure — `validate_workflow` won't flag it. Confirm presence by inspecting the execution.\n\n**n8n-mcp-tools-expert**: owns `n8n_manage_datatable` (Data Tables) and `n8n_executions` — use the latter to confirm a `binary` slot actually survived a given node.\n\n**n8n-error-handling**: storage uploads and downloads fail; the inbound/outbound staging steps need error branches so a missing key doesn't 404 silently.\n\n**using-n8n-mcp-skills**: the index of how these skills fit together.\n\n---\n\n## Verifying binary survived\n\nValidation won't catch a stripped binary slot — it's a silent failure. Confirm it ran correctly:\n\n1. `n8n_test_workflow` (or trigger a real run) to produce an execution.\n2. `n8n_executions` to pull that execution, and inspect per-node output for the `binary` slot — it shows presence and metadata even if the base64 is too large to render.\n3. The node where `binary` last appears is the node before the strip. That's where the pass-through or Merge goes.\n\n---\n\n## Quick Reference Checklist\n\n- [ ] File contents read from `$binary.<key>` — never `$json`\n- [ ] HTTP downloads use `responseFormat: \"file\"`\n- [ ] Code nodes re-attach `binary` on return when the file must continue\n- [ ] JSON transforms either pass binary through or Merge it back (`combineByPosition`)\n- [ ] No attempt to pass binary into/out of an agent tool — keys/URLs through JSON instead\n- [ ] `passthroughBinaryImages` used only for LLM vision, not as a tool channel\n- [ ] Chat-surface images uploaded to storage; the URL is embedded, not the bytes\n- [ ] Storage backend chosen with the user (not defaulted to S3); signed URLs for sensitive content\n- [ ] Binary presence confirmed by inspecting the execution, not by validation\n\n---\n\n**Remember**: two slots, side by side. Data rides in `$json`, files ride in `$binary` — and the moment a file has to cross an agent tool or reach a chat surface, it travels as a URL, not as bytes.\n","data/skills/n8n-agents/SKILL.md":"---\nname: n8n-agents\ndescription: Design n8n AI agents the right way. Use when building or editing any @n8n/n8n-nodes-langchain.* AI node — an AI Agent, LLM chain, Text Classifier, or Information Extractor — and whenever the user mentions AI agents, LLM with tools, tool calling, $fromAI, system prompts, agent memory, sessionId, structured/JSON output, output parser, RAG, vector store, a chat assistant/bot, or human-in-the-loop review. Covers Agent-vs-chain-vs-classifier choice, the model/memory/tools/outputParser slots, tool names/descriptions as prompt, structured output with autoFix, memory, RAG, human review, and chat topologies.\n---\n\n# n8n Agents\n\nThe n8n AI Agent node (`@n8n/n8n-nodes-langchain.agent`) is a multi-turn LLM driver with sub-nodes for the model, memory, tools, and an optional output parser. This skill is the **deep** guide to designing agents and the LangChain family around them. For the high-level \"where an agent fits in a workflow\" picture, see **n8n-workflow-patterns** `ai_agent_workflow.md` — this skill goes one level down into *how to build it well*.\n\nFor node-type formats: in workflow JSON the LangChain nodes use the long `@n8n/n8n-nodes-langchain.*` form (`.agent`, `.lmChatOpenAi`, `.memoryBufferWindow`, `.outputParserStructured`, `.toolWorkflow`, `.toolHttpRequest`, `.toolCode`). When you call `get_node` / `validate_node`, use the **short** form (`nodes-langchain.agent`). See **n8n-mcp-tools-expert** for the format rules.\n\n---\n\n## Pick the right node first\n\nReaching for an Agent when the task is one-shot classification or extraction is the most common over-build. Decide before you wire anything:\n\n| You need to… | Use | Why |\n|---|---|---|\n| Call tools, reason over multiple turns, or hold memory | **AI Agent** (`.agent`) | The full loop: model + tools + memory + optional parser. Also a fine default when you'd rather standardize. |\n| One-shot text in → text out, no tools | **Basic LLM Chain** (`.chainLlm`) | No agent loop, easier to debug. Still accepts an `outputParserStructured` sub-node. |\n| Route a natural-language input to one of **N branches** | **Text Classifier** (`.textClassifier`) | ONE node, N output handles, downstream wires directly into each. Not Agent + Switch. |\n| Pull structured fields out of free text | **Information Extractor** (`.informationExtractor`) | Purpose-built field extraction with a schema. |\n| 3-way positive/neutral/negative split | **Sentiment Analysis** (`.sentimentAnalysis`) | Built-in branch outputs. |\n| Condense a long document | **Summarization Chain** (`.chainSummarization`) | Map-reduce summarization built in. |\n| Generate an image / audio / video | **The provider's native single-call node** (OpenAI, Gemini, ElevenLabs…) | NEVER wrap media generation in an Agent — see \"Binary and the agent boundary\". |\n\n**Text Classifier detail (the Agent + Switch anti-pattern):** every category needs both a **name AND a description**. The model routes against the *description*, not the name — a category with no description gets picked by coin-flip. Set `options.enableAutoFixing: true` for robustness on edge inputs. One node, N branches, done. Reaching for an Agent that \"decides\" then a Switch that \"routes\" is two nodes plus prompt boilerplate for what Text Classifier does natively.\n\nChat-model nodes (`.lmChatOpenAi`, `.lmChatAnthropic`, `.lmChatOpenRouter`, …) are **sub-nodes** — they don't run standalone. They wire into a chain, agent, classifier, or extractor via the `ai_languageModel` connection.\n\n---\n\n## The sub-node pattern\n\nThe Agent has a **main input** (the prompt / user message) and up to four **sub-node slots**, each wired by its own `ai_*` connection type:\n\n| Slot | Connection type | Required? | Node example |\n|---|---|---|---|\n| **model** | `ai_languageModel` | Yes | `.lmChatOpenAi`, `.lmChatAnthropic`, `.lmChatOpenRouter` |\n| **memory** | `ai_memory` | Optional | `.memoryBufferWindow`, `.memoryPostgresChat` |\n| **tools** | `ai_tool` | Optional (but the point of an agent) | `slackTool`, `.toolWorkflow`, `.toolHttpRequest`, `.toolCode` |\n| **outputParser** | `ai_outputParser` | Optional | `.outputParserStructured` |\n\nA sub-node connects FROM itself TO the agent. In workflow JSON the connection lives on the **sub-node**, keyed by the `ai_*` type:\n\n```json\n\"Main LLM\": {\n  \"ai_languageModel\": [[{ \"node\": \"AI Agent\", \"type\": \"ai_languageModel\", \"index\": 0 }]]\n},\n\"Simple Memory\": {\n  \"ai_memory\": [[{ \"node\": \"AI Agent\", \"type\": \"ai_memory\", \"index\": 0 }]]\n},\n\"Search customer DB\": {\n  \"ai_tool\": [[{ \"node\": \"AI Agent\", \"type\": \"ai_tool\", \"index\": 0 }]]\n}\n```\n\nMultiple tools all connect into the same `ai_tool` index 0 — they stack, they don't fan into separate indices. With `n8n_update_partial_workflow` you wire each with an `addConnection` op using `sourceOutput: \"ai_tool\"`. The agent puts its final answer in **`$json.output`** (not `.text`, not `.response`) — downstream nodes read `{{ $json.output }}`.\n\nSee **EXAMPLES.md** for a complete stateless agent-core node-object snippet.\n\n---\n\n## Two non-negotiables\n\n1. **Tool names and descriptions ARE part of the prompt.** The model picks a tool by reading its name and description — nothing else. A tool named `tool1` with an empty description is invisible to the model: it skips it, mis-selects it, or hallucinates parameters. There's usually no error — just an agent that \"won't use my tool\". Treat both like API design. → **TOOLS.md**\n2. **Structured output must parse AND autoFix.** An `outputParserStructured` with `autoFix: true` and a **coding-capable fixer model** is the production pattern. Without autoFix, one malformed JSON response halts the whole workflow. → **STRUCTURED_OUTPUT.md**\n\n---\n\n## Strong defaults\n\n- **Per-tool usage goes in the tool description, not the system prompt.** Anything about *how to call this specific tool* belongs with the tool, so it travels across agents and keeps the system prompt focused. → **SYSTEM_PROMPT.md**\n- **Sub-workflow tools (`.toolWorkflow`) for anything multi-step.** Any workflow becomes a tool with typed `$fromAI()` inputs, and composes with branching, error handling, and reuse. Default here when in doubt. → **SUBWORKFLOW_AS_TOOL.md** and **n8n-subworkflows**.\n- **Wrap tools with user-visible side effects in human review.** Sends, payments, refunds, account changes get gated behind an approval node so a human signs off before the tool fires. → **HUMAN_REVIEW.md**\n- **Raise `maxIterations`.** The default tool-call cap is **low** (single digits on most versions) — fine for a one-tool agent, far too low for a multi-tool agent that chains several calls per turn. It surfaces as \"max iterations reached\" or empty output. Set `options.maxIterations` to a realistic ceiling (15 for a focused sub-agent, 50-200 for a broad orchestrator).\n- **Put the current date in the system prompt** via `{{ $now }}` (or `{{ $now.format('DDDD') }}`). A hardcoded date is stale immediately.\n\n---\n\n## The four tool types\n\nPick the lightest option that covers the job:\n\n| Tool type | Node | Use when |\n|---|---|---|\n| **Native tool node** | `slackTool`, `gmailTool`, `toolCalculator`, … | The capability maps to one existing node + one operation. Lowest overhead. |\n| **Sub-workflow as tool** | `.toolWorkflow` | More than one node, reusable logic, or you want independent testability. The canonical n8n way — **default when in doubt**. |\n| **HTTP Request Tool** | `.toolHttpRequest` | A single external HTTP API the agent should orchestrate directly. Reuse the service's predefined credential to cover operations a native node doesn't expose. |\n| **MCP Client Tool** | `.mcpClientTool` | A maintained MCP server already covers it, or you want one published workflow to serve many agents. |\n\nThere is also a **Custom Code Tool** (`.toolCode`) for pure inline computation — but its runtime contract (string in / string out, no `$fromAI`, no `$helpers`) is owned by the **n8n-code-tool** skill. Read that before writing one. Rule of thumb: if you find yourself reaching for `$fromAI()` inside the code, you want `.toolWorkflow` instead.\n\n### `$fromAI()`: how the agent fills tool parameters\n\nTool parameters the agent should decide are wrapped in `$fromAI()`. It is a **real n8n expression helper**, used inside a tool node's parameter expressions:\n\n```\n={{ $fromAI('paramName', 'what to put here — be specific: format, range, example', 'string') }}\n```\n\n- **paramName** — the name the model uses internally (snake_case or camelCase, be consistent).\n- **description** — tells the model what value to produce. **It is part of the prompt** — write it like JSDoc.\n- **type** (optional) — `'string'` (default), `'number'`, `'boolean'`, `'json'`. A wrong-typed value fails the call.\n- **defaultValue** (optional) — used when the model omits it.\n\n`$fromAI()` carries JSON only — it **cannot carry binary** (no base64, no file bytes). And not every parameter has to be `$fromAI`: plumb identity, authority limits, and correlation IDs (`userId`, refund caps, `sessionId`) deterministically from workflow context so the agent can't get them wrong or even see them. → **TOOLS.md** for the full anatomy and the \"give the agent a button, not a steering wheel\" pattern.\n\n---\n\n## System prompt vs tool description\n\n| Belongs in the **system prompt** | Belongs in the **tool's description** |\n|---|---|\n| Persona, role, voice | What this specific tool does |\n| Global output/format rules (\"respond in markdown\") | When to use it vs other tools |\n| Refusal / safety behavior | What each parameter means and its shape |\n| Display protocols (`![]()` for images) | Examples of good vs bad invocations |\n| Universal context (current date via `$now`, user role) | Tool-specific gotchas (rate limits, edge cases) |\n| Inter-tool flow (\"after generating, always display\") | Tool-specific input transformations |\n\nWhy split it: a well-described tool works in **any** agent that drops it in, tool details only \"load\" when the model considers that tool (token efficiency), and you update one tool description instead of a paragraph buried in a 5000-token prompt. → **SYSTEM_PROMPT.md**\n\n---\n\n## Structured output: when and how\n\nAdd an `outputParserStructured` sub-node (wired `ai_outputParser`) when downstream needs strict JSON, not free-form text. Two rules:\n\n1. **Use `schemaType: 'manual'` with a real JSON Schema, not `jsonSchemaExample`.** An example can't express required-vs-optional, enums, numeric ranges, or array constraints — you outgrow it the first time the shape gets non-trivial. Reach for `fromJson` + an example only for throwaway shapes.\n2. **`autoFix: true` with a coding-capable fixer model.** Wire a *second* model into the parser's `ai_languageModel` slot. Reconciling broken JSON against a schema is a coding task — a weak fixer just produces another malformed retry and burns tokens.\n\n→ **STRUCTURED_OUTPUT.md** for the schema patterns, the load-bearing \"DO NOT wrap in markdown\" retry line, and the parse-failure cookbook.\n\n---\n\n## Memory: brief mental model\n\nMemory is a sub-node (`ai_memory`). Without it, every call is stateless — correct for one-shot tasks (classify, summarize). With it, the agent holds a conversation, keyed by whatever expression you bind to `sessionKey`.\n\n- **`memoryBufferWindow`** — keeps the last N exchanges per key and persists across executions via n8n's store. The default for chat. **`contextWindowLength` defaults to 5, which is very low** — 50 is a saner starting point. Messages past the window are gone entirely.\n- **`memoryPostgresChat` / `memoryRedisChat`** — only when memory must be read *outside* the agent (your own UI, analytics, cross-system). Not needed just to survive restarts; BufferWindow already does that.\n\n**Plumb a stable key from the trigger to memory consistently.** Chat triggers fill `sessionId` automatically; for other surfaces derive one (Slack `thread_ts`, a webhook conversation ID). Never hardcode `sessionId: 'default'` and never put `sessionId` behind `$fromAI` (the model will fabricate a UUID). → **MEMORY.md**\n\n---\n\n## Binary and the agent boundary\n\nThis is the seam that trips people up:\n\n- **The model CAN see uploaded images** (vision) via `options.passthroughBinaryImages: true` on the agent.\n- **Tools CANNOT receive binary.** `$fromAI()` is JSON-only — no base64, no bytes, even through non-AI bindings.\n- **The agent's output is text-shaped** (or structured-text with a parser). When a model returns image/audio/video bytes, the Agent doesn't surface them at all — there's nothing to recover downstream.\n\n**Workaround:** pre-stage uploads to storage before the agent runs, inject the storage keys into the system prompt, and let tools accept the key as a string parameter and re-fetch internally. For one-shot media generation, skip the agent and call the provider's native single-call node directly.\n\nThe binary mechanics (which storage, how to stage, how to re-fetch) are owned by **n8n-binary-and-data** — see its agent-tool binary reference. This skill only marks the boundary; don't re-derive the mechanics here.\n\n---\n\n## Human review (gate destructive tools)\n\nWhen a tool's effect needs human sign-off before execution (sends, payments, refunds, account changes), wrap it with a review tool node — `slackHitlTool`, `discordHitlTool`, `telegramHitlTool`, `gmailHitlTool`, etc. (n8n names these \"Hitl\" / human-in-the-loop). The review node sits **between** the wrapped tool and the agent on the `ai_tool` connection: wrapped tool → review node → Agent.\n\nWhether sign-off is needed is a product/policy call — **surface the question to the user**, recommend based on blast radius, and let them decide.\n\n**The critical rule: show the actual parameters the wrapped tool will receive.** Use the literal `{{ $tool.parameters.<name> }}` in the approval message, never a `$fromAI()` paraphrase — otherwise the human approves text the model made up, not the call about to fire. → **HUMAN_REVIEW.md**\n\n---\n\n## Chat agents (Slack, Discord, Teams, Telegram)\n\n**The one non-negotiable, regardless of complexity:** any chat-triggered workflow that posts a reply MUST **filter out the bot's own user ID**, or its own replies re-trigger it in an infinite loop that burns runs and tokens. Prefer trigger-level filtering when available (Slack Trigger's `options.userIds` is an **exclusion list** — put the bot ID there); otherwise filter `$json.user !== '<BOT_USER_ID>'` in the first node after the trigger.\n\nBeyond the filter, a simple bot (trigger → agent → reply) lives fine in one workflow. Split into **shell + core + sub-agents** only once you need loading UX, sub-agents, multi-surface reuse, or robust error handling:\n\n- **Shell** — trigger, anti-loop filter, event-type Switch, loading/error UX, renders the reply. No LLM.\n- **Core** — stateless agent, `chatInput` + `threadId` inputs, memory keyed on `threadId`, tools and sub-agents.\n- **Sub-agents** — one narrow domain each, called via `.toolWorkflow`, **stateless** (full context in `chatInput`).\n\n→ **CHAT_AGENT_PATTERNS.md** for per-surface semantics, threading-as-session, and the full topology.\n\n---\n\n## Persisted n8n Agents (n8n_manage_agents)\n\nA **persisted n8n Agent** is a different artifact from the AI Agent node covered above: a standalone assistant record — model, instructions, tools, skills, tasks, memory, channels — stored and versioned by n8n itself, managed through `n8n_manage_agents` (n8n's instance-level MCP server), not a node inside a workflow's JSON.\n\n| You need to… | Use |\n|---|---|\n| A reasoning step inside a workflow, wired with `ai_*` sub-nodes | **AI Agent node** (this skill, above) |\n| A standalone assistant with its own lifecycle — draft, validate, publish, versions, channels — independent of any one workflow | **Persisted Agent** (`n8n_manage_agents`) |\n\n**Prerequisites:** `N8N_MCP_ACCESS_TOKEN` configured (separate from the Public API key) and n8n **2.34+** with the agents module enabled. The token is required for **every** action, including `reference`/`search` — without it, nothing works. Separately, `reference` and `search` work for **any** agent regardless of MCP exposure; every other action needs the target agent exposed to MCP (agents created through this tool are exposed automatically — the exposure gate only matters for agents that already existed before this tool touched them).\n\n**Build sequence:**\n1. `action: \"reference\"` — read the config schema and the exact mutate operations before anything else.\n2. `action: \"discover_assets\"` — list what the agent can actually be wired to. Takes `projectId` (from `n8n_list_catalog({kind: \"projects\"})`) and `kind`: `models` (with a `provider`), `integrations`, `workflows`, `subagents` or `mcpServers`. One call per kind.\n3. `action: \"create\"` — `projectId`, `name`, `config`.\n4. `action: \"mutate\"` — one resource per call (`config.patch`, `skill.upsert`/`delete`, `task.upsert`/`delete`, `customTool.upsert`/`delete`), always carrying the **latest** hash forward. Mind the two names: n8n returns it as `configHash` and expects it back as `args.baseConfigHash`. `args` are forwarded to n8n verbatim, so a near-miss on any field name comes back as `INVALID_ARGS`, not a helpful correction — which is why step 1 reads the schema first. A stale hash comes back as `STALE_CONFIG` — re-`get` and retry with the fresh one.\n5. `action: \"validate\"` — before offering to `call` or `publish`.\n6. `action: \"publish\"` — **only on the user's explicit request**, never proactively.\n\n`action: \"call\"` runs the agent with real credentials and real tools — a live execution, not a dry run. A result can carry `approvals[]` for tool calls that need a human decision; **never approve on the user's behalf** — surface them and resume only after the user decides.\n\n**Custom tools are a third code runtime — don't reuse either of the others.** A `customTool.upsert` body is **TypeScript**, and the only imports it may use are `@n8n/agents` and `zod`. This is not the Code node (JavaScript/Python, returns `[{json: …}]`) and not the AI-agent Custom Code Tool covered by **n8n-code-tool** (`@n8n/n8n-nodes-langchain.toolCode`, returns a string, no `$fromAI()`). Reaching for the wrong contract is the easy mistake here, because all three are \"write code the agent calls\". Read the shape from `action: \"reference\"` before writing one; a compile failure or an unknown `agentId` surfaces as `AGENT_TOOL_ERROR`.\n\n**Credential caveat:** on n8n 2.36.x the agents runtime rejects `azureOpenAiApi` and `aws` credentials (reported as `missing: [\"credential\"]`); the response's `hint` names the accepted types instead.\n\n**Testing without leaving debris:** name throwaway agents `[TEST] …` and `delete` them when you're done — a persisted Agent outlives the conversation that made it, unlike a workflow you can leave inactive.\n\n→ **n8n-mcp-tools-expert** `## Agents` for the tool's full action list and error codes.\n\n---\n\n## RAG (retrieval augmented generation)\n\nn8n ships the LangChain RAG primitives (document loaders, splitters, embeddings, vector stores, retrievers). Two opinions worth stating up front:\n\n1. **Rule out cheaper lookups first.** Exact lookups → a database or Data Table query, not RAG. Freshness → a live search tool. A small/structured doc set → give the agent list/fetch tools. Reach for a vector store only when there are too many docs to list and queries are semantic.\n2. **Wire the vector store as a retrieval tool** (`mode: 'retrieve-as-tool'`, `ai_tool`) so the agent decides when retrieval is relevant and can phrase the query itself. Embed query and documents with the **same** model.\n\n→ **RAG.md** (intentionally thin — defaults depend on data shape and scale).\n\n---\n\n## Reference files\n\n| File | Read when |\n|---|---|\n| **TOOLS.md** | Adding tools, choosing among the four types, writing names/descriptions, `$fromAI` anatomy |\n| **SUBWORKFLOW_AS_TOOL.md** | Wiring a sub-workflow as a tool via `.toolWorkflow`, mapping agent-filled vs plumbed params |\n| **SYSTEM_PROMPT.md** | Writing/refactoring a system prompt, the system-prompt-vs-tool-description split |\n| **STRUCTURED_OUTPUT.md** | Forcing JSON output, configuring autoFix, the fixer model, parse-failure fixes |\n| **MEMORY.md** | Choosing a memory type, persistence, sessionId handling |\n| **HUMAN_REVIEW.md** | Adding human approval, approval-message content, multi-channel approver |\n| **CHAT_AGENT_PATTERNS.md** | Building a Slack/Discord/Teams/Telegram bot, shell + core + sub-agents topology |\n| **RAG.md** | Retrieval-augmented agents (thin by design) |\n| **EXAMPLES.md** | Concrete node-object snippets: stateless agent core, Slack router shell, domain sub-agent |\n\n---\n\n## Anti-patterns\n\n| Anti-pattern | What goes wrong | Fix |\n|---|---|---|\n| Generic tool names (`tool1`, `doStuff`, `runQuery`) | Model can't tell which tool to pick — skips them or hallucinates params | Verb-first specific names: `Search customer database`, `Generate image with Veo` |\n| Empty or one-line tool descriptions | Model has no idea when to invoke; bad selection, no error | Write a real description: what it does, when to use, what each param means |\n| Cramming per-tool instructions into the system prompt | Bloated prompt, no reuse, per-tool guidance buried | Move tool-specific instructions into tool descriptions |\n| Agent + Switch to route on natural language | Two nodes + prompt boilerplate where Text Classifier is one node | Use Text Classifier — each category gets its own output handle (name **and** description) |\n| Wrapping image/audio/video generation in an Agent | Binary doesn't flow through tools or out of the agent output | Use the provider's native single-call node directly |\n| `outputParserStructured` without `autoFix` | One malformed response halts the workflow | `autoFix: true` + a coding-capable fixer model |\n| Passing binary directly to a tool | Doesn't work — binary can't cross the tool boundary | Pre-stage to storage, pass keys; see **n8n-binary-and-data** |\n| Hardcoded `sessionId` / no sessionId / `sessionId` behind `$fromAI` | Conversations cross, or the model fabricates a UUID | Plumb a stable key from the trigger to memory and tools |\n| Two near-identical tools | Selection is non-deterministic, model gets confused | One tool with internal branching driven by a parameter |\n| Chat bot with no bot-user filter | Its own replies re-trigger it → infinite loop | Exclude the bot user ID at the trigger or first node |\n| `maxIterations` left at the low default on a multi-tool agent | \"Max iterations reached\" / empty output | Raise `options.maxIterations` |\n| Filling the human-review message via `$fromAI()` | Approver signs off on a paraphrase, not the real call | Use literal `{{ $tool.parameters.<name> }}` |\n\n---\n\n## What's NOT available via the community MCP\n\n| Want to do | Reality |\n|---|---|\n| Chat-test a workflow's AI Agent node end-to-end interactively | `n8n_test_workflow` runs the workflow, but a true multi-turn chat session against the node is a UI activity (canvas chat tester). A persisted Agent, by contrast, can be run live via `n8n_manage_agents` `call` — see \"Persisted n8n Agents\" above. |\n| Set credentials' actual secret values | `n8n_manage_credentials` creates/updates credential records, but the agent provider keys themselves are entered/verified in the UI. |\n| Assign a workflow's Error Workflow | UI only — see **n8n-error-handling**. Build the catch-all, then hand the user the UI step. |\n| Pin the exact model availability per instance | Model lists shift between versions — `search_nodes`/`get_node` reflect what's installed. Verify on the target instance. |\n\nWhat the MCP **can** do: search and inspect every LangChain node (`search_nodes`, `get_node`), validate node config and the whole graph (`validate_node`, `validate_workflow`), build and patch the agent and its sub-nodes (`n8n_update_partial_workflow` with `addConnection` on `ai_*` outputs), test (`n8n_test_workflow`), and pull the saved JSON to verify wiring (`n8n_get_workflow`). The deep AI-agent guide also lives in `tools_documentation({topic: \"ai_agents_guide\", depth: \"full\"})`.\n\n---\n\n## Integration with other skills\n\n- **n8n-workflow-patterns** (`ai_agent_workflow.md`) — the high-level \"agent in a workflow\" shape. This skill is the deep dive; start there for architecture.\n- **n8n-mcp-tools-expert** — node-type formats (short form for `get_node`, long form in JSON) and tool-selection guidance. Consult before any MCP call.\n- **n8n-node-configuration** — `displayOptions`-driven fields on the agent and sub-nodes; Slack/Block Kit message shapes (`NODE_FAMILY_GOTCHAS.md`, Slack section).\n- **n8n-expression-syntax** — `{{ }}`, `$json.output`, `$now`, and `$fromAI`/`$tool.parameters` all rely on correct expression syntax.\n- **n8n-code-tool** — the Custom Code Tool's runtime contract (string in/out, no `$fromAI`). Read it before writing a `.toolCode`.\n- **n8n-subworkflows** — the sub-workflow primitive that `.toolWorkflow` builds on (Execute Workflow Trigger inputs/outputs, naming, search-before-build).\n- **n8n-binary-and-data** — owns the agent-tool binary boundary mechanics (staging uploads, returning generated files).\n- **n8n-validation-expert** — interpreting `validate_workflow` results, including AI-connection issues (a tool wired into `main` instead of `ai_tool` flags as disconnected).\n- **n8n-error-handling** — `onError: 'continueErrorOutput'` on tool sub-workflows and the agent-core call; error UX on chat shells.\n- **n8n-code-javascript / n8n-code-python** — for Code-node logic *inside* a tool sub-workflow (different sandbox from the Code Tool).\n\n---\n\n## Quick reference checklist\n\nBefore shipping an agent:\n\n- [ ] **Right node**: Agent for tools/memory/multi-turn; Text Classifier for routing; Information Extractor for fields; native node for media\n- [ ] **Model** wired via `ai_languageModel`\n- [ ] **Every tool** has a verb-first specific name AND a real description\n- [ ] **`$fromAI()` descriptions** are specific (format, range, example); identity/limits/sessionId plumbed deterministically, not via `$fromAI`\n- [ ] **Per-tool guidance** lives in tool descriptions, not the system prompt\n- [ ] **`$now`** in the system prompt (no hardcoded date)\n- [ ] **`maxIterations`** raised for multi-tool agents\n- [ ] **Memory** keyed on a stable `sessionKey` from the trigger (not `'default'`, not `$fromAI`); `contextWindowLength` raised from 5\n- [ ] **Structured output**: `schemaType: 'manual'` + `autoFix: true` + a coding-capable fixer model\n- [ ] **Destructive tools** wrapped in human review; approval message uses `$tool.parameters`, not `$fromAI`\n- [ ] **Chat bots** filter the bot's own user ID (trigger-level or first node)\n- [ ] **Binary**: model vision via `passthroughBinaryImages`; tools get storage keys, never bytes\n- [ ] **Validated** with `validate_workflow` and verified with `n8n_get_workflow` (sub-nodes on `ai_*`, not `main`)\n\n---\n\n**Remember**: an agent is only as good as its tool names, descriptions, and system-prompt discipline. The model can't see your wiring — it sees a system prompt and a list of named, described tools. Design those like an API and most \"the agent won't behave\" problems disappear.\n","data/skills/n8n-code-python/SKILL.md":"---\nname: n8n-code-python\ndescription: Write Python code in n8n Code nodes. Use when writing Python in n8n, using _input/_json/_node syntax, working with standard library, or need to understand Python limitations in n8n Code nodes. Use this skill when the user specifically requests Python for an n8n Code node. Note — JavaScript is recommended for 95% of use cases — only use Python when the user explicitly prefers it or the task requires Python-specific standard library capabilities (regex, hashlib, statistics). EXCEPTION — for Python in the AI-agent-callable Custom Code Tool (@n8n/n8n-nodes-langchain.toolCode), use the n8n-code-tool skill instead (input is _query, return must be a string).\n---\n\n# Python Code Node (Beta)\n\nExpert guidance for writing Python code in n8n Code nodes.\n\n---\n\n## ⚠️ Important: JavaScript First\n\n**Recommendation**: Use **JavaScript for 95% of use cases**. Only use Python when:\n- You need specific Python standard library functions\n- You're significantly more comfortable with Python syntax\n- You're doing data transformations better suited to Python\n\n**Why JavaScript is preferred:**\n- Full n8n helper functions (`this.helpers.httpRequest`, etc.)\n- Luxon DateTime library for advanced date/time operations\n- No external library limitations\n- Better n8n documentation and community support\n\n---\n\n## Quick Start\n\n```python\n# Basic template for Python Code nodes\nitems = _input.all()\n\n# Process data\nprocessed = []\nfor item in items:\n    processed.append({\n        \"json\": {\n            **item[\"json\"],\n            \"processed\": True,\n            \"timestamp\": datetime.now().isoformat()\n        }\n    })\n\nreturn processed\n```\n\n### Essential Rules\n\n1. **Consider JavaScript first** - Use Python only when necessary\n2. **Access data**: `_input.all()`, `_input.first()`, or `_input.item`\n3. **CRITICAL**: Must return `[{\"json\": {...}}]` format\n4. **CRITICAL**: Webhook data is under `_json[\"body\"]` (not `_json` directly)\n5. **CRITICAL LIMITATION**: **No external libraries** (no requests, pandas, numpy)\n6. **Standard library only**: json, datetime, re, base64, hashlib, urllib.parse, math, random, statistics\n\n---\n\n## Mode Selection Guide\n\nSame as JavaScript - choose based on your use case:\n\n### Run Once for All Items (Recommended - Default)\n\n**Use this mode for:** 95% of use cases\n\n- **How it works**: Code executes **once** regardless of input count\n- **Data access**: `_input.all()` or `_items` array (Native mode)\n- **Best for**: Aggregation, filtering, batch processing, transformations\n- **Performance**: Faster for multiple items (single execution)\n\n```python\n# Example: Calculate total from all items\nall_items = _input.all()\ntotal = sum(item[\"json\"].get(\"amount\", 0) for item in all_items)\n\nreturn [{\n    \"json\": {\n        \"total\": total,\n        \"count\": len(all_items),\n        \"average\": total / len(all_items) if all_items else 0\n    }\n}]\n```\n\n### Run Once for Each Item\n\n**Use this mode for:** Specialized cases only\n\n- **How it works**: Code executes **separately** for each input item\n- **Data access**: `_input.item` or `_item` (Native mode)\n- **Best for**: Item-specific logic, independent operations, per-item validation\n- **Performance**: Slower for large datasets (multiple executions)\n\n```python\n# Example: Add processing timestamp to each item\nitem = _input.item\n\nreturn [{\n    \"json\": {\n        **item[\"json\"],\n        \"processed\": True,\n        \"processed_at\": datetime.now().isoformat()\n    }\n}]\n```\n\n---\n\n## Python Modes: Beta vs Native\n\nn8n offers two Python execution modes:\n\n### Python (Beta) - Recommended\n- **Use**: `_input`, `_json`, `_node` helper syntax\n- **Best for**: Most Python use cases\n- **Helpers available**: `_now`, `_today`, `_jmespath()`\n- **Import**: `from datetime import datetime`\n\n```python\n# Python (Beta) example\nitems = _input.all()\nnow = _now  # Built-in datetime object\n\nreturn [{\n    \"json\": {\n        \"count\": len(items),\n        \"timestamp\": now.isoformat()\n    }\n}]\n```\n\n### Python (Native) (Beta)\n- **Use**: `_items`, `_item` variables only\n- **No helpers**: No `_input`, `_now`, etc.\n- **More limited**: Standard Python only\n- **Use when**: Need pure Python without n8n helpers\n\n```python\n# Python (Native) example\nprocessed = []\n\nfor item in _items:\n    processed.append({\n        \"json\": {\n            \"id\": item[\"json\"].get(\"id\"),\n            \"processed\": True\n        }\n    })\n\nreturn processed\n```\n\n**Recommendation**: Use **Python (Beta)** for better n8n integration.\n\n---\n\n## Data Access Patterns\n\nAccess input data through underscore-prefixed variables. Each item is a dict shaped `{\"json\": {...}}`, so the actual fields live under `[\"json\"]`.\n\n```python\n# Pattern 1: _input.all() - Most common. Arrays, batch ops, aggregations\nall_items = _input.all()            # list of {\"json\": {...}} dicts\n\n# Pattern 2: _input.first() - Very common. Single objects, API responses\ndata = _input.first()[\"json\"]       # built-in safety vs all_items[0]\n\n# Pattern 3: _input.item - \"Run Once for Each Item\" mode ONLY\ncurrent = _input.item[\"json\"]       # None/error in All Items mode\n\n# Pattern 4: _node - Reference a specific named node\nwebhook_data = _node[\"Webhook\"][\"json\"]\nhttp_data = _node[\"HTTP Request\"][\"json\"]\n```\n\n**See**: [DATA_ACCESS.md](DATA_ACCESS.md) for the comprehensive guide — six `_input.all()` recipes (filter, transform, aggregate, sort, group, deduplicate), `_input.first()` and `_input.item` examples, multi-node combining, the JS-vs-Python variable table, and the decision tree.\n\n---\n\n## Critical: Webhook Data Structure\n\n**MOST COMMON MISTAKE**: Webhook data is nested under `[\"body\"]`\n\n```python\n# ❌ WRONG - Will raise KeyError\nname = _json[\"name\"]\nemail = _json[\"email\"]\n\n# ✅ CORRECT - Webhook data is under [\"body\"]\nname = _json[\"body\"][\"name\"]\nemail = _json[\"body\"][\"email\"]\n\n# ✅ SAFER - Use .get() for safe access\nwebhook_data = _json.get(\"body\", {})\nname = webhook_data.get(\"name\")\n```\n\n**Why**: Webhook node wraps all request data under `body` property. This includes POST data, query parameters, and JSON payloads.\n\n**See**: [DATA_ACCESS.md](DATA_ACCESS.md) for full webhook structure details\n\n---\n\n## Return Format Requirements\n\n**CRITICAL RULE**: Always return list of dictionaries with `\"json\"` key\n\n### Correct Return Formats\n\n```python\n# ✅ Single result\nreturn [{\n    \"json\": {\n        \"field1\": value1,\n        \"field2\": value2\n    }\n}]\n\n# ✅ Multiple results\nreturn [\n    {\"json\": {\"id\": 1, \"data\": \"first\"}},\n    {\"json\": {\"id\": 2, \"data\": \"second\"}}\n]\n\n# ✅ List comprehension\ntransformed = [\n    {\"json\": {\"id\": item[\"json\"][\"id\"], \"processed\": True}}\n    for item in _input.all()\n    if item[\"json\"].get(\"valid\")\n]\nreturn transformed\n\n# ✅ Empty result (when no data to return)\nreturn []\n\n# ✅ Conditional return\nif should_process:\n    return [{\"json\": processed_data}]\nelse:\n    return []\n```\n\n### Incorrect Return Formats\n\n```python\n# ❌ WRONG: Dictionary without list wrapper\nreturn {\n    \"json\": {\"field\": value}\n}\n\n# ❌ WRONG: List without json wrapper\nreturn [{\"field\": value}]\n\n# ❌ WRONG: Plain string\nreturn \"processed\"\n\n# ❌ WRONG: Incomplete structure\nreturn [{\"data\": value}]  # Should be {\"json\": value}\n```\n\n**Why it matters**: Next nodes expect list format. Incorrect format causes workflow execution to fail.\n\n**See**: [ERROR_PATTERNS.md](ERROR_PATTERNS.md) #2 for detailed error solutions\n\n---\n\n## Critical Limitation: No External Libraries\n\n**MOST IMPORTANT PYTHON LIMITATION**: Cannot import external packages on default installs.\n\n> **Self-hosted exception**: external package availability depends entirely on the instance's Python runner configuration. If the user states their self-hosted instance has specific packages available in the Python runner environment, use them — don't refuse. When unsure, ask or write standard-library-only code.\n\n**❌ NOT available** (raise `ModuleNotFoundError`): `requests`, `pandas`, `numpy`, `scipy`, `bs4`/BeautifulSoup, `lxml`.\n\n**✅ Available** (standard library only): `json`, `datetime`, `re`, `base64`, `hashlib`, `urllib.parse`, `math`, `random`, `statistics`.\n\n### Workarounds\n\n**Need HTTP requests?**\n- ✅ Use **HTTP Request node** before Code node\n- ✅ Or switch to **JavaScript** and use `this.helpers.httpRequest()` (the bare `$helpers` global is undefined in the task-runner sandbox)\n\n**Need data analysis (pandas/numpy)?**\n- ✅ Use Python **statistics** module for basic stats\n- ✅ Or switch to **JavaScript** for most operations\n- ✅ Manual calculations with lists and dictionaries\n\n**Need web scraping (BeautifulSoup)?**\n- ✅ Use **HTTP Request node** + **HTML Extract node**\n- ✅ Or switch to **JavaScript** with regex/string methods\n\n**See**: [STANDARD_LIBRARY.md](STANDARD_LIBRARY.md) for complete reference\n\n---\n\n## Common Patterns Overview\n\nBased on production workflows, the most useful Python patterns are:\n\n1. **Data Transformation** - Transform all items with list comprehensions\n2. **Filtering & Aggregation** - Sum, filter, count with built-in functions\n3. **String Processing with Regex** - Extract patterns from text with `re`\n4. **Data Validation** - Validate and clean data, attach error lists\n5. **Statistical Analysis** - Calculate mean/median/stdev with the `statistics` module\n\nCopy-ready snippets for all five live in [COMMON_PATTERNS.md](COMMON_PATTERNS.md#quick-pattern-snippets), alongside 10 fully detailed production patterns (multi-source aggregation, markdown parsing, JSON comparison, CRM normalization, dictionary lookup, top-N filtering, and more).\n\n---\n\n## Error Prevention - Top 5 Mistakes\n\n1. **Importing external libraries** (Python-specific) → `import requests` raises `ModuleNotFoundError`. Use the HTTP Request node or JavaScript instead.\n2. **Empty code or missing return** → every path must end with `return [{\"json\": ...}]`.\n3. **Incorrect return format** → wrap in a list: `{\"json\": {...}}` becomes `[{\"json\": {...}}]`.\n4. **KeyError on dictionary access** → use `.get()`: `_json.get(\"user\", {}).get(\"name\", \"Unknown\")`.\n5. **Webhook body nesting** → read via `[\"body\"]`: `_json.get(\"body\", {}).get(\"email\", \"no-email\")`.\n\n**See**: [ERROR_PATTERNS.md](ERROR_PATTERNS.md) for the comprehensive guide — each error with wrong-vs-right code, error messages, nested-access fixes, an `AttributeError` bonus case, a prevention checklist, and a quick-fix table.\n\n---\n\n## Standard Library Reference\n\nMost useful modules: `json` (parse/generate), `datetime` (dates + `timedelta`), `re` (regex), `base64` (encode/decode), `hashlib` (hashing), `urllib.parse` (URL ops), and `statistics` (mean/median/stdev). Also available: `math`, `random`, `collections`, `itertools`, `functools`.\n\nFor a condensed cheat sheet plus full per-module examples, see [STANDARD_LIBRARY.md](STANDARD_LIBRARY.md#quick-reference-most-useful-modules).\n\n---\n\n## Best Practices\n\n### 1. Always Use .get() for Dictionary Access\n\n```python\n# ✅ SAFE: Won't crash if field missing\nvalue = item[\"json\"].get(\"field\", \"default\")\n\n# ❌ RISKY: Crashes if field doesn't exist\nvalue = item[\"json\"][\"field\"]\n```\n\n### 2. Handle None/Null Values Explicitly\n\n```python\n# ✅ GOOD: Default to 0 if None\namount = item[\"json\"].get(\"amount\") or 0\n\n# ✅ GOOD: Check for None explicitly\ntext = item[\"json\"].get(\"text\")\nif text is None:\n    text = \"\"\n```\n\n### 3. Use List Comprehensions for Filtering\n\n```python\n# ✅ PYTHONIC: List comprehension\nvalid = [item for item in items if item[\"json\"].get(\"active\")]\n\n# ❌ VERBOSE: Manual loop\nvalid = []\nfor item in items:\n    if item[\"json\"].get(\"active\"):\n        valid.append(item)\n```\n\n### 4. Return Consistent Structure\n\n```python\n# ✅ CONSISTENT: Always list with \"json\" key\nreturn [{\"json\": result}]  # Single result\nreturn results  # Multiple results (already formatted)\nreturn []  # No results\n```\n\n### 5. Debug with print() Statements\n\n```python\n# Debug statements appear in browser console (F12)\nitems = _input.all()\nprint(f\"Processing {len(items)} items\")\nprint(f\"First item: {items[0] if items else 'None'}\")\n```\n\n---\n\n## Production Gotchas\n\n### SplitInBatches Loop Semantics\n\nThe SplitInBatches node has two outputs:\n- `main[0]` = **done** — fires ONCE after all batches complete\n- `main[1]` = **each batch** — fires for every batch (the loop body)\n\nAlways add a **Limit 1** node after the done output.\n\n### Correct Node Reference Syntax\n\n```python\n# ❌ WRONG\ndata = _node['HTTP Request']['json']\n\n# ✅ CORRECT - call .first() then access json\ndata = _node['HTTP Request'].first()['json']\n```\n\n### Cross-Iteration Data Not Available in Python\n\n`$getWorkflowStaticData('global')` may not be available in Python Beta mode. If you need to accumulate data across SplitInBatches iterations, use a JavaScript Code node for the accumulation logic instead.\n\n---\n\n## When to Use Python vs JavaScript\n\n### Use Python When:\n- ✅ You need `statistics` module for statistical operations\n- ✅ You're significantly more comfortable with Python syntax\n- ✅ Your logic maps well to list comprehensions\n- ✅ You need specific standard library functions\n\n### Use JavaScript When:\n- ✅ You need HTTP requests (`this.helpers.httpRequest()`)\n- ✅ You need advanced date/time (DateTime/Luxon)\n- ✅ You want better n8n integration\n- ✅ **For 95% of use cases** (recommended)\n\n### Consider Other Nodes When:\n- ❌ Simple field mapping → Use **Set** node\n- ❌ Basic filtering → Use **Filter** node\n- ❌ Simple conditionals → Use **IF** or **Switch** node\n- ❌ HTTP requests only → Use **HTTP Request** node\n\n---\n\n## Integration with Other Skills\n\n### Works With:\n\n**n8n Expression Syntax**:\n- Expressions use `{{ }}` syntax in other nodes\n- Code nodes use Python directly (no `{{ }}`)\n- When to use expressions vs code\n\n**n8n MCP Tools Expert**:\n- How to find Code node: `search_nodes({query: \"code\"})`\n- Get configuration help: `get_node({nodeType: \"nodes-base.code\"})`\n- Validate code: `validate_node({nodeType: \"nodes-base.code\", config: {...}})`\n\n**n8n Node Configuration**:\n- Mode selection (All Items vs Each Item)\n- Language selection (Python vs JavaScript)\n- Understanding property dependencies\n\n**n8n Workflow Patterns**:\n- Code nodes in transformation step\n- When to use Python vs JavaScript in patterns\n\n**n8n Validation Expert**:\n- Validate Code node configuration\n- Handle validation errors\n- Auto-fix common issues\n\n**n8n Code JavaScript**:\n- When to use JavaScript instead\n- Comparison of JavaScript vs Python features\n- Migration from Python to JavaScript\n\n---\n\n## Quick Reference Checklist\n\nBefore deploying Python Code nodes, verify:\n\n- [ ] **Considered JavaScript first** - Using Python only when necessary\n- [ ] **Code is not empty** - Must have meaningful logic\n- [ ] **Return statement exists** - Must return list of dictionaries\n- [ ] **Proper return format** - Each item: `{\"json\": {...}}`\n- [ ] **Data access correct** - Using `_input.all()`, `_input.first()`, or `_input.item`\n- [ ] **No external imports** - Only standard library (json, datetime, re, etc.)\n- [ ] **Safe dictionary access** - Using `.get()` to avoid KeyError\n- [ ] **Webhook data** - Access via `[\"body\"]` if from webhook\n- [ ] **Mode selection** - \"All Items\" for most cases\n- [ ] **Output consistent** - All code paths return same structure\n\n---\n\n## Additional Resources\n\n### Related Files\n- [DATA_ACCESS.md](DATA_ACCESS.md) - Comprehensive Python data access patterns\n- [COMMON_PATTERNS.md](COMMON_PATTERNS.md) - 10 Python patterns for n8n\n- [ERROR_PATTERNS.md](ERROR_PATTERNS.md) - Top 5 errors and solutions\n- [STANDARD_LIBRARY.md](STANDARD_LIBRARY.md) - Complete standard library reference\n\n### n8n Documentation\n- Code Node Guide: https://docs.n8n.io/code/code-node/\n- Python in n8n: https://docs.n8n.io/code/builtin/python-modules/\n\n---\n\n**Ready to write Python in n8n Code nodes - but consider JavaScript first!** Use Python for specific needs, reference the error patterns guide to avoid common mistakes, and leverage the standard library effectively.\n","data/skills/n8n-code-javascript/SKILL.md":"---\nname: n8n-code-javascript\ndescription: Write JavaScript code in n8n Code nodes. Use when writing JavaScript in n8n, using $input/$json/$node syntax, making HTTP requests with this.helpers / the $helpers global, working with dates using DateTime, troubleshooting Code node errors, choosing between Code node modes, or doing any custom data transformation in n8n. Always use this skill when a workflow needs a Code node — whether for data aggregation, filtering, API calls, format conversion, batch processing logic, or any custom JavaScript. Covers SplitInBatches loop patterns, cross-iteration data, pairedItem, and real-world production patterns. Also use when asked why a Code node or workflow is slow, which execution mode is faster, or how to cut per-item overhead on large datasets. EXCEPTION — for the AI-agent-callable Custom Code Tool (@n8n/n8n-nodes-langchain.toolCode, a tool attached to an AI Agent), use the n8n-code-tool skill instead; it has a different runtime contract.\n---\n\n# JavaScript Code Node\n\nExpert guidance for writing JavaScript code in n8n Code nodes.\n\n---\n\n## Quick Start\n\n```javascript\n// Basic template for Code nodes\nconst items = $input.all();\n\n// Process data\nconst processed = items.map(item => ({\n  json: {\n    ...item.json,\n    processed: true,\n    timestamp: new Date().toISOString()\n  }\n}));\n\nreturn processed;\n```\n\n### Essential Rules\n\n1. **Choose \"Run Once for All Items\" mode** (recommended for most use cases)\n2. **Access data**: `$input.all()`, `$input.first()`, or `$input.item`\n3. **Return `[{json: {...}}]`** — the canonical, mode-portable form. In *Run Once for All Items* mode n8n also auto-wraps a bare `return {…}` object, so that runs too; what genuinely fails is returning a primitive (string/number) or `null`.\n4. **CRITICAL**: Webhook data is under `$json.body` (not `$json` directly)\n5. **Built-ins available**: `this.helpers.httpRequest()` (no auth — the bare `$helpers` global is **undefined** in the task-runner sandbox, so `$helpers.httpRequest()` throws `ReferenceError: $helpers is not defined`), DateTime (Luxon), $jmespath(). **Not available**: `this.helpers.httpRequestWithAuthentication` (deny-listed), $env (when N8N_BLOCK_ENV_ACCESS_IN_NODE=true), require() (unless allowlisted). For anything beyond a trivial unauthenticated GET (auth, pagination, retries), prefer the **HTTP Request node** and keep Code nodes for pure logic.\n6. **Instance-allowlisted libraries**: Self-hosted instances can allowlist modules via `N8N_RUNNERS_ALLOWED_BUILT_IN_MODULES` and `N8N_RUNNERS_ALLOWED_EXTERNAL_MODULES` (legacy: `NODE_FUNCTION_ALLOW_BUILTIN` / `NODE_FUNCTION_ALLOW_EXTERNAL`). If the user says their instance allows specific modules (e.g. `axios`, `lodash`, `crypto`), use them via `require()` — don't refuse. If unsure, ask or default to built-ins only.\n7. **Wrong skill?** If you're writing code for a **Custom Code Tool** attached to an AI Agent (`@n8n/n8n-nodes-langchain.toolCode`), stop — that node has a different contract (input via `query`, must return a string, no `$input`/`$helpers`). Use the **n8n-code-tool** skill.\n\n---\n\n## Mode Selection Guide\n\nThe Code node offers two execution modes. Choose based on your use case:\n\n### Run Once for All Items (Recommended - Default)\n\n**Use this mode for:** 95% of use cases\n\n- **How it works**: Code executes **once** regardless of input count\n- **Data access**: `$input.all()` or `items` array\n- **Best for**: Aggregation, filtering, batch processing, transformations, API calls with all data\n- **Performance**: Faster for multiple items (single execution)\n\n```javascript\n// Example: Calculate total from all items\nconst allItems = $input.all();\nconst total = allItems.reduce((sum, item) => sum + (item.json.amount || 0), 0);\n\nreturn [{\n  json: {\n    total,\n    count: allItems.length,\n    average: total / allItems.length\n  }\n}];\n```\n\n**When to use:**\n- ✅ Comparing items across the dataset\n- ✅ Calculating totals, averages, or statistics\n- ✅ Sorting or ranking items\n- ✅ Deduplication\n- ✅ Building aggregated reports\n- ✅ Combining data from multiple items\n\n### Run Once for Each Item\n\n**Use this mode for:** Specialized cases only\n\n- **How it works**: Code executes **separately** for each input item\n- **Data access**: `$input.item` or `$item`\n- **Best for**: Item-specific logic, independent operations, per-item validation\n- **Performance**: Slower for large datasets (multiple executions)\n\n```javascript\n// Example: Add processing timestamp to each item\nconst item = $input.item;\n\nreturn [{\n  json: {\n    ...item.json,\n    processed: true,\n    processedAt: new Date().toISOString()\n  }\n}];\n```\n\n**When to use:**\n- ✅ Each item needs independent API call\n- ✅ Per-item validation with different error handling\n- ✅ Item-specific transformations based on item properties\n- ✅ When items must be processed separately for business logic\n\n**Decision Shortcut:**\n- **Need to look at multiple items?** → Use \"All Items\" mode\n- **Each item completely independent?** → Use \"Each Item\" mode\n- **Not sure?** → Use \"All Items\" mode (you can always loop inside)\n\n### Why \"All Items\" is faster — the per-item boundary\n\nMode choice is the single biggest performance lever in a Code node. Each *per-item* execution context costs a setup tax (measured on n8n 2.x, small records):\n\n| What runs per item | Approx. cost |\n|---|---|\n| Code **All Items** (one run for the whole set) | ~0.02 ms/item |\n| Expression in any node (IF / Set / etc.) | ~0.2 ms/item |\n| Code **Each Item** (a full sandbox per item) | ~0.6 ms/item — ~25–30× All Items |\n\nSo `Run Once for Each Item` over 10k items is ~6 s of pure overhead vs ~0.2 s in `Run Once for All Items`. Use Each Item only when an item genuinely needs isolating (independent error handling, or a per-item API call you can't batch); otherwise loop *inside* one All Items node. Expression complexity itself is essentially free (~90% of the cost is the per-item context, not your code) and every node→node hop re-copies all items — so reduce the *number* of per-item boundaries, don't micro-optimize each one. Below a few hundred items none of this matters; reach for it on the hot path (large item counts, little I/O).\n\n**See**: [DATA_ACCESS.md](DATA_ACCESS.md) → \"Mode Performance\" for the corollaries, hop costs, and scale check.\n\n---\n\n## Data Access Patterns\n\nFour ways to pull data from upstream nodes. Note `$node[\"Name\"]` and `$('Name')` need `.first().json` or `.all()` — never `.json` directly.\n\n```javascript\nconst allItems = $input.all();          // 1. All items — batch ops, aggregation (most common)\nconst data = $input.first().json;       // 2. First item — single objects, API responses\nconst item = $input.item;               // 3. Current item — \"Each Item\" mode ONLY (undefined otherwise)\nconst other = $node[\"Webhook\"].json;    // 4. Named node — combine data across nodes\n```\n\nAlways access fields via `.json` (e.g. `item.json.name`, not `item.name`), and prefer the explicit `$input.first().json.field` over a bare `$json.field`.\n\n**See**: [DATA_ACCESS.md](DATA_ACCESS.md) for the full guide — every pattern with examples, a decision tree, and the common mistakes (mutating originals, missing length checks, `$input.item` in the wrong mode).\n\n---\n\n## Critical: Webhook Data Structure\n\n**MOST COMMON MISTAKE**: Webhook data is nested under `.body`\n\n```javascript\n// ❌ WRONG - Will return undefined\nconst name = $json.name;\nconst email = $json.email;\n\n// ✅ CORRECT - Webhook data is under .body\nconst name = $json.body.name;\nconst email = $json.body.email;\n\n// Or with $input\nconst webhookData = $input.first().json.body;\nconst name = webhookData.name;\n```\n\n**Why**: Webhook node wraps all request data under `body` property. This includes POST data, query parameters, and JSON payloads.\n\n**See**: [DATA_ACCESS.md](DATA_ACCESS.md) for full webhook structure details\n\n---\n\n## Return Format Requirements\n\n**Canonical form**: `[{json: {...}}]` — an array of objects each with a `json` property. It is unambiguous and works identically in both execution modes, so make it your default.\n\nIn *Run Once for All Items* mode n8n auto-normalizes looser shapes on the way out: a single bare object, or an array of bare objects, gets wrapped under `json` for you. So `return {foo: 1}` runs. What has nothing to wrap — and therefore genuinely fails at runtime with \"Code doesn't return items properly\" — is a primitive (string/number/boolean) or `null`/`undefined`. (n8n-mcp ≥ 2.63.0 no longer flags a bare-object return as an error; it reflects this auto-wrap behavior.)\n\n### Correct Return Formats\n\n```javascript\n// ✅ Single result\nreturn [{\n  json: {\n    field1: value1,\n    field2: value2\n  }\n}];\n\n// ✅ Multiple results\nreturn [\n  {json: {id: 1, data: 'first'}},\n  {json: {id: 2, data: 'second'}}\n];\n\n// ✅ Transformed array\nconst transformed = $input.all()\n  .filter(item => item.json.valid)\n  .map(item => ({\n    json: {\n      id: item.json.id,\n      processed: true\n    }\n  }));\nreturn transformed;\n\n// ✅ Empty result (when no data to return)\nreturn [];\n\n// ✅ Conditional return\nif (shouldProcess) {\n  return [{json: processedData}];\n} else {\n  return [];\n}\n```\n\n### Non-Canonical Returns (auto-wrapped — prefer the canonical form)\n\n```javascript\n// ⚠️ Auto-wrapped in All Items mode → [{json: {field: value}}]. Runs, but prefer the array form.\nreturn {\n  json: {field: value}\n};\n\n// ⚠️ Auto-wrapped → [{json: {field: value}}]. Runs, but add the json wrapper for clarity.\nreturn [{field: value}];\n\n// ✅ Fine — input items already carry a json property, so returning them unchanged is a valid passthrough\nreturn $input.all();\n```\n\n### Genuinely Broken Returns\n\n```javascript\n// ❌ FAILS: primitive — n8n errors \"Code doesn't return items properly\"\nreturn \"processed\";\n\n// ❌ FAILS: null / undefined — nothing to pass to the next node\nreturn null;\n```\n\n**Why it matters**: The canonical `[{json: {...}}]` is unambiguous and behaves the same in both modes. n8n auto-normalizes bare objects and arrays-of-objects in All Items mode, but a primitive or `null` return has nothing to wrap and stops execution.\n\n**See**: [ERROR_PATTERNS.md](ERROR_PATTERNS.md) #3 for detailed error solutions\n\n---\n\n## Common Patterns Overview\n\nThe most useful Code node shapes from production workflows. One quick example — sum/aggregate across all items:\n\n```javascript\nconst items = $input.all();\nconst total = items.reduce((sum, item) => sum + (item.json.amount || 0), 0);\nreturn [{ json: { total, count: items.length, average: total / items.length } }];\n```\n\nThe full library covers 10 patterns: multi-source aggregation, regex filtering, markdown/structured-text parsing, JSON comparison, CRM/form transformation, release processing, array transformation with computed fields, Slack Block Kit formatting, top-N ranking, and string-aggregation reporting — each with variations.\n\n**See**: [COMMON_PATTERNS.md](COMMON_PATTERNS.md) for the 10 detailed production patterns (and the Best Practices section: validate input, try-catch, filter-early, array methods over loops, console.log debugging).\n\n---\n\n## Error Prevention - Top Mistakes\n\nThe recurring Code node failures, in rough frequency order:\n\n1. **Empty code / missing return** — always end with `return [...]`, and make sure *every* branch returns.\n2. **Expression syntax as code** — don't write `{{ }}` where JavaScript belongs (`return {{ $json.x }}` is a syntax error). Use `` `${$json.field}` `` or `$input.first().json.field`. `{{ }}` *inside a string literal* is fine — it's just literal text n8n won't evaluate.\n3. **Return shape** — prefer `return [{json:{...}}]`. A bare `return {…}` auto-wraps in All Items mode, but returning a primitive (string/number) or `null` is what actually fails.\n4. **Missing null checks** — use optional chaining: `item.json?.user?.email || 'fallback'`.\n5. **Webhook body nesting** — `$json.email` is undefined; use `$json.body.email`.\n6. **Auth helpers blocked** (`httpRequestWithAuthentication`) and `$env` blocked — route secrets through credentials/HTTP Request node, not the Code node sandbox.\n\n**See**: [ERROR_PATTERNS.md](ERROR_PATTERNS.md) for the comprehensive guide — each error with wrong/right code, escaping rules, the sandbox restrictions (Errors #6–#7), a prevention checklist, and a quick error-message lookup table.\n\n---\n\n## Built-in Functions & Helpers\n\n```javascript\n// HTTP requests (no auth — see sandbox note below)\nconst res = await this.helpers.httpRequest({ method: 'GET', url: 'https://api.example.com/data' });\n\n// DateTime (Luxon): now, formatting, arithmetic\nconst now = DateTime.now();\nconst formatted = now.toFormat('yyyy-MM-dd');\nconst tomorrow = now.plus({ days: 1 });\n\n// $jmespath() — query JSON structures\nconst adults = $jmespath($input.first().json, 'users[?age >= `18`]');\n\n// $getWorkflowStaticData() — data that persists across executions\n```\n\n**Sandbox (since n8n v2.0, JsTaskRunnerSandbox):** the accessor is `this.helpers.httpRequest()` — the bare `$helpers` global is **undefined** here (`$helpers.httpRequest()` throws `ReferenceError`). Inside a nested async function where `this` is lost, call it as `await fn.call(this, ...)`. `this.helpers.httpRequestWithAuthentication` and `this.helpers.requestWithAuthenticationPaginated` are deny-listed (→ `UnsupportedFunctionError`); for authenticated calls use an **HTTP Request node** with the credential (preferred), a sub-workflow, or a manual `Authorization: Bearer ${token}` header on `this.helpers.httpRequest()` only when the token already flows through the workflow as data. `$env` is blocked when `N8N_BLOCK_ENV_ACCESS_IN_NODE=true`; `require()` works only for allowlisted modules. `Buffer`, `URL`, and standard JS globals (Math, JSON, Object, Array) always work.\n\n**See**: [BUILTIN_FUNCTIONS.md](BUILTIN_FUNCTIONS.md) for the complete reference — full httpRequest options, all DateTime/Luxon operations, JMESPath patterns, static-data use cases, and the sandbox-restriction details.\n\n---\n\n## Best Practices\n\n- **Validate input first** — guard for empty arrays / missing `.json` before processing.\n- **Try-catch risky work** (HTTP calls) and return an error object instead of crashing.\n- **Prefer array methods** (`filter`/`map`/`reduce`) over manual loops.\n- **Filter early, transform late** — shrink the dataset before expensive work.\n- **Descriptive names** and `console.log()` for debugging (output goes to the browser console).\n\n**See**: [COMMON_PATTERNS.md](COMMON_PATTERNS.md) → \"Best Practices\" for code examples of each.\n\n---\n\n## Production Gotchas\n\nHard-won lessons from real deployments — summarized here, with code in [DATA_ACCESS.md](DATA_ACCESS.md) → \"Production Gotchas\":\n\n- **SplitInBatches outputs are counterintuitive**: `main[0]` = **done** (fires once, after all batches), `main[1]` = **each batch** (the loop body). Add a **Limit 1** node after the done output as a safety.\n- **Iteration count is the cost**: each loop iteration re-runs the whole body through the engine (~0.8 ms overhead each). `batchSize: 1` is the loop equivalent of *Each Item* — use the largest batch your real constraint (rate limit, page size, memory) allows, or don't loop at all.\n- **Cross-iteration accumulation (CRITICAL)**: after the loop, `$('Node Inside Loop').all()` returns ONLY the last iteration's items. Accumulate via `$getWorkflowStaticData('global')` (reset before, push inside, read after).\n- **pairedItem**: when emitting items that don't map 1:1 to input, set `pairedItem: { item: i }` or downstream Set nodes fail with `paired_item_no_info`.\n- **Node reference syntax**: `$('Node').first().json` or `$('Node').all()` — never `.json` directly on the reference.\n- **Float precision**: compare currency at the cent level — `Math.round(a*100) !== Math.round(b*100)` — to avoid false positives from float noise.\n\n---\n\n## When to Use Code Node\n\n> **Before reaching for a Code node, walk the transform gatekeeper** in the n8n Expression Syntax skill: expression → arrow-function IIFE inside an Edit Fields field → Code node, in that order. The first two paths cover most \"transform this data\" tasks at ~1–10ms each, versus the Code node's sandboxed ~500–1000ms — a ~100x gap on pure single-item shaping, with no functional difference. The Code node earns its place only for whole-dataset aggregation (`$input.all()`), allowlisted libraries, or async work. And before writing code for crypto (HMAC, hashing, signing) or XML/SOAP/RSS parsing, check for a **native node** — n8n has a Crypto node (`nodes-base.crypto`) and an XML node (`nodes-base.xml`) that cover those without any JavaScript. Dropping into Code for something a native node already does is one of the most common false positives.\n\nUse Code node when:\n- ✅ Complex transformations requiring multiple steps\n- ✅ Custom calculations or business logic\n- ✅ Recursive operations\n- ✅ API response parsing with complex structure\n- ✅ Multi-step conditionals\n- ✅ Data aggregation across items\n\nConsider other nodes when:\n- ❌ Simple field mapping → Use **Set** node\n- ❌ Basic filtering → Use **Filter** node\n- ❌ Simple conditionals → Use **IF** or **Switch** node\n- ❌ HTTP requests only → Use **HTTP Request** node\n\n**Code node excels at**: Complex logic that would require chaining many simple nodes\n\n---\n\n## Integration with Other Skills\n\n### Works With:\n\n**n8n Expression Syntax**:\n- Expressions use `{{ }}` syntax in other nodes\n- Code nodes use JavaScript directly (no `{{ }}`)\n- When to use expressions vs code\n\n**n8n MCP Tools Expert**:\n- How to find Code node: `search_nodes({query: \"code\"})`\n- Get configuration help: `get_node({nodeType: \"nodes-base.code\"})`\n- Validate code: `validate_node({nodeType: \"nodes-base.code\", config: {...}})`\n\n**n8n Node Configuration**:\n- Mode selection (All Items vs Each Item)\n- Language selection (JavaScript vs Python)\n- Understanding property dependencies\n\n**n8n Workflow Patterns**:\n- Code nodes in transformation step\n- Webhook → Code → API pattern\n- Error handling in workflows\n\n**n8n Validation Expert**:\n- Validate Code node configuration\n- Handle validation errors\n- Auto-fix common issues\n\n---\n\n## Quick Reference Checklist\n\nBefore deploying Code nodes, verify:\n\n- [ ] **Code is not empty** - Must have meaningful logic\n- [ ] **Return statement exists** - Returns items, not a primitive/`null`\n- [ ] **Canonical return format** - Each item: `{json: {...}}` (bare objects auto-wrap, but be explicit)\n- [ ] **Data access correct** - Using `$input.all()`, `$input.first()`, or `$input.item`\n- [ ] **No `{{ }}` written as code** - Use JavaScript template literals: `` `${value}` ``\n- [ ] **Error handling** - Guard clauses for null/undefined inputs\n- [ ] **Webhook data** - Access via `.body` if from webhook\n- [ ] **Mode selection** - \"All Items\" for most cases\n- [ ] **Performance** - Prefer map/filter over manual loops\n- [ ] **Output consistent** - All code paths return same structure\n\n---\n\n## Additional Resources\n\n### Related Files\n- [DATA_ACCESS.md](DATA_ACCESS.md) - Comprehensive data access patterns\n- [COMMON_PATTERNS.md](COMMON_PATTERNS.md) - 10 production-tested patterns\n- [ERROR_PATTERNS.md](ERROR_PATTERNS.md) - Top 5 errors and solutions\n- [BUILTIN_FUNCTIONS.md](BUILTIN_FUNCTIONS.md) - Complete built-in reference\n\n### n8n Documentation\n- Code Node Guide: https://docs.n8n.io/code/code-node/\n- Built-in Methods: https://docs.n8n.io/code-examples/methods-variables-reference/\n- Luxon Documentation: https://moment.github.io/luxon/\n\n---\n\n**Ready to write JavaScript in n8n Code nodes!** Start with simple transformations, use the error patterns guide to avoid common mistakes, and reference the pattern library for production-ready examples.\n","data/skills/n8n-mcp-tools-expert/SKILL.md":"---\nname: n8n-mcp-tools-expert\ndescription: Expert guide for using n8n-mcp MCP tools effectively. Use when searching for nodes, validating configurations, accessing templates, managing workflows, organizing workflows into folders, managing credentials, auditing instance security, or using any n8n-mcp tool. Provides tool selection guidance, parameter formats, and common patterns. IMPORTANT — Always consult this skill before calling any n8n-mcp tool — it prevents common mistakes like wrong nodeType formats, incorrect parameter structures, and inefficient tool usage. If the user mentions n8n, workflows, nodes, or automation and you have n8n MCP tools available, use this skill first.\n---\n\n# n8n MCP Tools Expert\n\nMaster guide for using n8n-mcp MCP server tools to build workflows.\n\n---\n\n## Tool Categories\n\nn8n-mcp provides tools organized into categories:\n\n1. **Node Discovery** → [SEARCH_GUIDE.md](SEARCH_GUIDE.md)\n2. **Configuration Validation** → [VALIDATION_GUIDE.md](VALIDATION_GUIDE.md)\n3. **Workflow Management** → [WORKFLOW_GUIDE.md](WORKFLOW_GUIDE.md)\n4. **Template Library** - Search and deploy 2,700+ real workflows\n5. **Data Tables** - Manage n8n data tables, rows and columns (`n8n_manage_datatable`)\n6. **Workflow Folders** - Folder CRUD + workflow placement (`n8n_manage_folders`)\n7. **Credential Management** - Full credential CRUD + schema discovery (`n8n_manage_credentials`)\n8. **Security & Audit** - Instance security auditing with custom deep scan (`n8n_audit_instance`)\n9. **Documentation & Guides** - Tool docs, AI agent guide, Code node guides\n10. **Agents** - Create, configure, validate, run and publish persisted n8n Agents (`n8n_manage_agents`, requires `N8N_MCP_ACCESS_TOKEN`)\n11. **Node Resource Resolution** - Resolve live dropdown/resource-locator values with a real credential (`n8n_explore_node_resources`, requires `N8N_MCP_ACCESS_TOKEN`)\n12. **Instance Catalog** - List projects and tags (`n8n_list_catalog`)\n\n---\n\n## Quick Reference\n\n### Most Used Tools (by success rate)\n\n| Tool | Use When | Speed |\n|------|----------|-------|\n| `search_nodes` | Finding nodes by keyword | <20ms |\n| `get_node` | Understanding node operations (detail=\"standard\") | <10ms |\n| `validate_node` | Checking configurations (mode=\"full\") | <100ms |\n| `n8n_create_workflow` | Creating workflows | 100-500ms |\n| `n8n_update_partial_workflow` | Editing workflows (MOST USED!) | 50-200ms |\n| `validate_workflow` | Checking complete workflow | 100-500ms |\n| `n8n_deploy_template` | Deploy template to n8n instance | 200-500ms |\n| `n8n_manage_datatable` | Managing data tables and rows | 50-500ms |\n| `n8n_manage_folders` | Folder CRUD + organizing workflows | 100-500ms |\n| `n8n_manage_credentials` | Credential CRUD + schema discovery | 50-500ms |\n| `n8n_audit_instance` | Security audit (built-in + custom scan) | 500-5000ms |\n| `n8n_autofix_workflow` | Auto-fix validation errors | 200-1500ms |\n| `n8n_manage_agents` | Persisted n8n Agent CRUD/validate/publish | 150-400ms; `call` action: 5-60s |\n| `n8n_explore_node_resources` | Resolve live loadOptions/listSearch values | 200 ms - 5 s |\n| `n8n_list_catalog` | List projects or tags | 50-300ms |\n\n---\n\n## Tool Selection Guide\n\n### Finding the Right Node\n\n**Workflow**:\n```\n1. search_nodes({query: \"keyword\"})\n2. get_node({nodeType: \"nodes-base.name\"})\n3. [Optional] get_node({nodeType: \"nodes-base.name\", mode: \"docs\"})\n```\n\n**Example**:\n```javascript\n// Step 1: Search\nsearch_nodes({query: \"slack\"})\n// Returns: nodes-base.slack\n\n// Step 2: Get details\nget_node({nodeType: \"nodes-base.slack\"})\n// Returns: operations, properties, examples (standard detail)\n\n// Step 3: Get readable documentation\nget_node({nodeType: \"nodes-base.slack\", mode: \"docs\"})\n// Returns: markdown documentation\n```\n\n**Common pattern**: search → get_node (18s average)\n\n### Validating Configuration\n\n**Workflow**:\n```\n1. validate_node({nodeType, config: {}, mode: \"minimal\"}) - Check required fields\n2. validate_node({nodeType, config, profile: \"runtime\"}) - Full validation\n3. [Repeat] Fix errors, validate again\n```\n\n**Common pattern**: validate → fix → validate (23s thinking, 58s fixing per cycle)\n\n### Managing Workflows\n\n**Workflow**:\n```\n1. n8n_create_workflow({name, nodes, connections})\n2. n8n_validate_workflow({id})\n3. n8n_update_partial_workflow({id, operations: [...]})\n4. n8n_validate_workflow({id}) again\n5. n8n_update_partial_workflow({id, operations: [{type: \"activateWorkflow\"}]})\n```\n\n**Common pattern**: iterative updates (56s average between edits)\n\n### Critical: Node JSON Hygiene When Creating Workflows\n\nThree structural mistakes in generated node JSON break the n8n UI even when the workflow validates:\n\n1. **Never emit a `credentials` block with a placeholder ID.** A fake ID like `\"id\": \"REPLACE_ME\"` renders the credential selector permanently disabled and non-clickable in the n8n UI (\"No credentials yet\") — the user has to recreate the node from scratch. If you don't know the real credential ID, **omit the `credentials` block entirely**; an absent block shows a normal empty dropdown the user can click. Use `n8n_manage_credentials({action: \"list\"})` to discover real credential IDs first.\n\n```javascript\n// ❌ Breaks the credential selector\n\"credentials\": {\"httpHeaderAuth\": {\"id\": \"REPLACE_ME\", \"name\": \"My API Key\"}}\n\n// ✅ Unknown ID → omit credentials block; user picks in UI\n// ✅ Known ID (from n8n_manage_credentials list) → use the real ID\n```\n\n2. **Generate UUID v4 values for node `id`** — not human-readable strings like `\"http-list-node\"`. n8n's frontend uses node IDs for form binding and credential component initialization; non-UUID IDs cause subtle UI breakage.\n\n3. **Use the current `typeVersion`** for each node — check `get_node` rather than hardcoding remembered versions (e.g. httpRequest is at 4.4+, not 4.2).\n\n---\n\n## Critical: nodeType Formats\n\n**Two different formats** for different tools!\n\n### Format 1: Search/Validate Tools\n```javascript\n// Use SHORT prefix\n\"nodes-base.slack\"\n\"nodes-base.httpRequest\"\n\"nodes-base.webhook\"\n\"nodes-langchain.agent\"\n```\n\n**Tools that use this**:\n- search_nodes (returns this format)\n- get_node\n- validate_node\n- validate_workflow\n\n### Format 2: Workflow Tools\n```javascript\n// Use FULL prefix\n\"n8n-nodes-base.slack\"\n\"n8n-nodes-base.httpRequest\"\n\"n8n-nodes-base.webhook\"\n\"@n8n/n8n-nodes-langchain.agent\"\n```\n\n**Tools that use this**:\n- n8n_create_workflow\n- n8n_update_partial_workflow\n\n### Conversion\n\n```javascript\n// search_nodes returns BOTH formats\n{\n  \"nodeType\": \"nodes-base.slack\",          // For search/validate tools\n  \"workflowNodeType\": \"n8n-nodes-base.slack\"  // For workflow tools\n}\n```\n\n---\n\n## Common Mistakes\n\nEight recurring mistakes. Two are worth showing in full because they silently corrupt structure:\n\n```javascript\n// nodeType prefix (search/validate tools want the SHORT form)\nget_node({nodeType: \"slack\"})              // ❌ missing prefix → \"Node not found\"\nget_node({nodeType: \"n8n-nodes-base.slack\"}) // ❌ FULL prefix is for workflow tools\nget_node({nodeType: \"nodes-base.slack\"})     // ✅\n\n// credentials must be nested by type with {id, name} — not a flat string\nupdates: {credentials: \"myApiKey\"}                              // ❌\nupdates: {credentials: {httpHeaderAuth: {id: \"abc123\", name: \"My API Key\"}}}  // ✅\n```\n\n| # | Mistake | Fix |\n|---|---------|-----|\n| 1 | Wrong nodeType format | SHORT `nodes-base.*` for search/validate; FULL `n8n-nodes-base.*` for workflow tools (see above) |\n| 2 | `detail: \"full\"` by default | Default `standard` covers 95%; reach for `docs`/`search_properties` instead of `full` |\n| 3 | No validation profile | Pass `profile: \"runtime\"` explicitly (`minimal`/`ai-friendly`/`strict` for other stages) |\n| 4 | Ignoring auto-sanitization | ALL nodes sanitized on ANY update (operator structures, IF/Switch metadata); it can't fix broken connections or branch-count mismatches |\n| 5 | Not using smart parameters | Use `branch: \"true\"` / `case: 0` instead of fragile `sourceIndex` math |\n| 6 | Omitting `intent` | Always include `intent` on `n8n_update_partial_workflow` for better responses |\n| 7 | `parameters` instead of `updates` | `updateNode` takes `updates: {...}`, not `parameters: {...}` |\n| 8 | Wrong credential format | Nest by type with `{id, name}` (see above) |\n\nFull WRONG/CORRECT examples for each: see [VALIDATION_GUIDE.md → Common Mistakes](VALIDATION_GUIDE.md).\n\n---\n\n## Tool Usage Patterns\n\nThree patterns dominate real usage. Worked, step-by-step examples for each live in the reference guides.\n\n- **Pattern 1 — Node Discovery** (18s avg between steps): `search_nodes({query})` → `get_node({nodeType, includeExamples: true})`. See [SEARCH_GUIDE.md](SEARCH_GUIDE.md).\n- **Pattern 2 — Validation Loop** (23s thinking, 58s fixing): `validate_node({profile: \"runtime\"})` → read `errors` → fix config → validate again until clean. See [VALIDATION_GUIDE.md](VALIDATION_GUIDE.md).\n- **Pattern 3 — Workflow Editing** (99.0% success, 56s avg between edits): iterate `n8n_update_partial_workflow` (with `intent`) → `n8n_validate_workflow` → finally `activateWorkflow`. Build iteratively, NOT one-shot. See [WORKFLOW_GUIDE.md](WORKFLOW_GUIDE.md).\n\n---\n\n## Detailed Guides\n\n### Node Discovery Tools\nSee [SEARCH_GUIDE.md](SEARCH_GUIDE.md) for:\n- search_nodes\n- get_node with detail levels (minimal, standard, full)\n- get_node modes (info, docs, search_properties, versions)\n\n### Validation Tools\nSee [VALIDATION_GUIDE.md](VALIDATION_GUIDE.md) for:\n- Validation profiles explained\n- validate_node with modes (minimal, full)\n- validate_workflow complete structure\n- Auto-sanitization system\n- Handling validation errors\n\n### Workflow Management\nSee [WORKFLOW_GUIDE.md](WORKFLOW_GUIDE.md) for:\n- n8n_create_workflow\n- n8n_update_partial_workflow (21 operation types including patchNodeField, setNodeGroups, and moveToFolder!)\n- Smart parameters (branch, case)\n- AI connection types (8 types)\n- Workflow activation (activateWorkflow/deactivateWorkflow)\n- n8n_deploy_template\n- n8n_workflow_versions\n- n8n_manage_folders (folder CRUD + workflow placement)\n- n8n_manage_credentials (credential CRUD + schema discovery)\n- n8n_audit_instance (security auditing)\n\n### Templates, Data Tables & Self-Help\nSee [OPERATIONS_GUIDE.md](OPERATIONS_GUIDE.md) for:\n- search_templates / get_template / n8n_deploy_template examples\n- n8n_manage_datatable (full actions, filter conditions, examples)\n- tools_documentation, ai_agents_guide, n8n_health_check\n\n---\n\n## Template Usage\n\nThe 2,700+ template library has three tools: `search_templates` (modes `query`/`by_nodes`/`by_task`/`by_metadata`), `get_template` (modes `structure`/`full`), and `n8n_deploy_template` (deploys to your instance with `autoFix`/`autoUpgradeVersions`, returns workflow ID + required credentials + fixes applied).\n\nSee [OPERATIONS_GUIDE.md](OPERATIONS_GUIDE.md) for full search/get/deploy examples.\n\n---\n\n## Running Workflows\n\n`n8n_test_workflow` has one required parameter (`workflowId`) and a `method` that picks the path:\n\n| `method` | Backend | What it does |\n|---|---|---|\n| `auto` (default) | Public API | Detects a webhook/form/chat trigger and fires it over HTTP — the workflow must be **active**. No such trigger → it reports that the workflow cannot be triggered and names the methods below. **`auto` never runs anything through n8n's MCP server.** |\n| `trigger` | Public API | Same HTTP path, requested explicitly. |\n| `prepare` | n8n's MCP server | Read-only: lists the nodes that need pinned data. |\n| `pinned` | n8n's MCP server | Runs the workflow with `pinData` standing in for trigger, credentialed and HTTP Request nodes, and waits. Every other node still runs. A run that finishes in `error`/`crashed`/`canceled` comes back as `EXECUTION_FAILED` with the `executionId`. |\n| `direct` | n8n's MCP server | Starts a run and returns once it has started; nothing is pinned, so every node runs. `message` or `data`/`headers` are forwarded to the trigger as input. |\n\n- The last three need `N8N_MCP_ACCESS_TOKEN` (n8n 2.34+) and the workflow's \"Available in MCP\" setting.\n- `pinData` is keyed by node **name**, and every value is an array of items wrapped as `{\"json\": {...}}` — `{\"Webhook\": [{\"json\": {\"id\": \"123\"}}]}`, never a flat object. It must be non-empty.\n- `triggerNodeName` picks the trigger node to start from (defaults to the detected one; n8n requires it whenever inputs are given).\n- **Both run methods execute the workflow's nodes for real.** `direct` runs every node; `pinned` pins only trigger nodes, nodes with credentials and HTTP Request nodes, so Code, Set, If and credential-free I/O (Execute Command, file read/write) still run. Confirm with the user before running a workflow that writes anywhere.\n- `executionMode` applies to `direct`: `manual` (default) or `production`. It changes the execution context, not whether the run has side effects — a production run goes through the production execution path and is recorded as one. Only pass it when the user asked for one.\n- `timeoutMs` is the client deadline for the official call (5000-600000; default 30000 for `prepare`, 300000 for `pinned`/`direct`).\n- `direct` returns as soon as the run starts, so it reports success with an `executionId` regardless of how the run ends — poll `n8n_executions({action: \"get\", id: executionId})` for the outcome. A dispatch n8n refuses outright comes back as `OFFICIAL_MCP_ERROR`, not `EXECUTION_FAILED`.\n- A workflow whose \"Available in MCP\" setting is off answers `WORKFLOW_NOT_EXPOSED`; `exposeToMcp: true` turns the setting on and retries once. That is a visible, persistent change — ask the user first, and note that enabling it is itself a workflow update, so it can overwrite a concurrent UI edit.\n\nSuccessful and routed responses state `method` and `backend` (`public-api` or `official-mcp`); an envelope rejected on argument validation may carry neither.\n\nSee [WORKFLOW_GUIDE.md](WORKFLOW_GUIDE.md#n8n_test_workflow-running-workflows) for runnable examples of each method.\n\n---\n\n## Version History\n\n`n8n_workflow_versions` reads two independent histories, selected with `source`:\n\n- `source: \"local\"` (default) — the snapshots n8n-mcp takes before it changes a workflow. Any n8n version, no token, ids are numbers. Blind to edits made in the n8n UI. The only source that supports `delete` and `prune`.\n- `source: \"native\"` — n8n's own workflow history, the same list the UI shows, including edits made by people. Needs `N8N_MCP_ACCESS_TOKEN` (n8n 2.34+; the native `diff` needs 2.36, where `get_workflow_versions_diff` shipped) and the workflow's \"Available in MCP\" setting; ids are opaque strings; `list` is capped at 50 with an `offset`; `delete` and `prune` are refused with `MODE_NOT_SUPPORTED_FOR_SOURCE` (n8n owns that retention). Native rollback is not pre-validated — `validateBefore` is accepted and ignored.\n\n`mode: \"diff\"` compares two versions (`versionId` + `toVersionId`, both from the same source and workflow). A local diff (`data.format: \"n8n-mcp\"`) reports added/removed/modified nodes as node **IDs**; a native diff (`data.format: \"n8n\"`) is n8n's own payload with field-level before/after values. Branch on `data.format` rather than assuming field names.\n\nNative modes hit the same consent gate as the routed run methods: a workflow whose \"Available in MCP\" setting is off answers `WORKFLOW_NOT_EXPOSED`, and re-running with `exposeToMcp: true` turns that setting on and retries once (the response then carries `exposedToMcp: true`). It is a visible, persistent change to the workflow — ask the user before passing it. `timeoutMs` (5000-600000) is the client deadline for the native call.\n\nSee [WORKFLOW_GUIDE.md](WORKFLOW_GUIDE.md#n8n_workflow_versions-version-control) for every mode with runnable examples of both sources.\n\n---\n\n## Data Table Management\n\n`n8n_manage_datatable` is the MCP tool for managing data tables and rows from *outside* a workflow (table actions `createTable`/`listTables`/`getTable`/`updateTable`/`deleteTable`; row actions `getRows`/`insertRows`/`updateRows`/`upsertRows`/`deleteRows`, with filtering, pagination, and `dryRun`). Don't confuse it with the in-workflow `nodes-base.dataTable` node, which reads/writes rows *during execution* (see [n8n-node-configuration → OPERATION_PATTERNS.md](../n8n-node-configuration/OPERATION_PATTERNS.md#data-table-nodes-basedatatable)). Rule of thumb: MCP tool to set up a table once, workflow node to read/write on every execution. `deleteRows` requires a filter; use `dryRun: true` before bulk changes.\n\n**Column actions** — `addColumn`, `deleteColumn`, `renameColumn` — change an existing table's columns, which the Public API cannot do; they run through n8n's MCP server and need `N8N_MCP_ACCESS_TOKEN` (n8n 2.34+). `addColumn` takes `column: {name, type}` (name starts with a letter, letters/digits/underscores only, at most 63 chars; type is `string`, `number`, `boolean` or `date`); `deleteColumn`/`renameColumn` take the `columnId` from `getTable`, and `renameColumn` puts the new column name in `name`. They address the table by project: `projectId` is resolved automatically when exactly one project is accessible, otherwise the call returns `PROJECT_REQUIRED` and lists the candidates — pass `projectId` (from `n8n_list_catalog({kind: \"projects\"})`) to skip resolution. Renaming the *table* is not a column action: use `updateTable` on the Public API.\n\n**`deleteColumn` drops the column's values along with the column, and there is no undo.** That bites hardest where you'd least expect it: a column's type cannot be changed after creation, so \"make this column a number\" really means drop-and-re-add, which throws away everything in it. Read the values out with `getRows` first if they matter, and confirm with the user before dropping a populated column.\n\nSee [OPERATIONS_GUIDE.md](OPERATIONS_GUIDE.md) for all actions, filter conditions, and examples.\n\n---\n\n## Workflow Folders\n\n`n8n_manage_folders` organizes workflows into folders (actions `create`/`list`/`get`/`rename`/`move`/`delete`; n8n 2.19+, registered free Community tier and up). `projectId` defaults to `'personal'`. Placing workflows happens in the *workflow* tools: `parentFolderId` on `n8n_create_workflow`, or the `moveToFolder` operation of `n8n_update_partial_workflow` (both n8n 2.32+; `null` = project root). Two things to internalize: a workflow's folder is **write-only** in n8n's API (verify placement via a folder's `get` counts, never by reading the workflow), and `delete` without `transferToFolderId` **archives** the folder's workflows (`transferToFolderId: \"0\"` moves them to the project root instead, keeping them active).\n\nSee [WORKFLOW_GUIDE.md](WORKFLOW_GUIDE.md) for all actions, list filters/counts, and the delete semantics.\n\n---\n\n## Credential Management\n\n`n8n_manage_credentials` is the unified credential tool: actions `list`, `get`, `create`, `update`, `delete`, `getSchema`. It never returns secrets — `get`/`create`/`update` strip the `data` field. Use `getSchema` before `create` to discover required fields. The optional `includeUsage: true` flag (on `list`/`get`) reverse-scans workflows and attaches `usedIn: [{id, name, active}]` + `usageCount` — use it before deleting or rotating a credential to see what breaks (it triggers a full client-side scan, caps at 5000 workflows, excludes archived, and degrades to a `usageScanError` field on failure).\n\nSee [WORKFLOW_GUIDE.md](WORKFLOW_GUIDE.md) for all actions, the includeUsage shape, security notes, and the safe delete/rotate workflow.\n\n---\n\n## Agents\n\nThe three tools in this section exist only for n8n's instance-level MCP server (a separate endpoint from the Public API). `n8n_manage_agents` and `n8n_explore_node_resources` need `N8N_MCP_ACCESS_TOKEN`; `n8n_list_catalog` works without it and uses the token only for its team-project fallback. Other tools route individual operations through the same server — `n8n_test_workflow` `prepare`/`pinned`/`direct`, `n8n_workflow_versions` `source: \"native\"`, the `n8n_manage_datatable` column actions — as described in their own sections; see \"Tool Availability\" below.\n\n- `n8n_manage_agents` — create, configure, validate, run and publish persisted n8n Agents (a standalone assistant artifact: model, instructions, tools, skills, tasks, memory, channels — not the AI Agent workflow node). Actions: `reference`, `search`, `get`, `create`, `mutate`, `validate`, `call`, `publish`, `unpublish`, `revert`, `versions`, `delete`, `discover_assets`, `verify_mcp_server`, `update_integration`. Start with `action: \"reference\"`, then `discover_assets` → `create` → `mutate` (one resource at a time, always the latest hash — n8n returns it as `configHash` and expects it back as `args.baseConfigHash`; a stale one comes back as `STALE_CONFIG`) → `validate`. `publish` only on explicit request; `call` runs the agent with real credentials and tools and may return `approvals[]` for the human to decide. `timeoutMs` is a top-level parameter (default 30000, 180000 for `call`), not part of `args`. Needs n8n **2.34+** with the agents module; on 2.36.x the agents runtime rejects `azureOpenAiApi`/`aws` credentials. Envelope error codes: `NOT_CONFIGURED`, `INVALID_ARGS`, `STALE_CONFIG`, `AGENT_NOT_RUNNABLE`, `AGENT_TOOL_ERROR` (a custom tool that failed to compile, or an unknown `agentId`), plus the shared `OFFICIAL_MCP_*` family (`AUTH_FAILED`, `NOT_ENABLED`, `RATE_LIMITED`, `TOOL_UNAVAILABLE`, `URL_REJECTED`, `TIMEOUT`, `TRANSPORT_ERROR`, `ERROR`). See **n8n-agents** skill's \"Persisted n8n Agents\" section for the full workflow.\n- `n8n_explore_node_resources` — resolve the real values behind a node's `loadOptions` dropdown or resource-locator `listSearch` (Slack channels, Google Sheets tabs, model lists) using a live credential, instead of guessing an ID. Use it when `get_node` (`standard` detail) shows `dynamicOptions: {methodName, methodType, dependsOn}` on a property. **Six parameters are required and none of them are inferred:** `nodeType` (LONG form), `version` (the node `typeVersion` the method belongs to), `methodName` and `methodType` copied verbatim from `dynamicOptions`, and `credentialType` plus a `credentialId` of that type from `n8n_manage_credentials({action: \"list\"})`. Whatever the method `dependsOn` goes in `currentNodeParameters`, resource-locator values keeping their `{__rl: true, mode: \"id\", value: \"…\"}` shape. Each result's `value` is what belongs in the workflow parameter; `name` is display text only.\n- `n8n_list_catalog` — list instance-level `projects` (personal project marked, gives `projectId` for `n8n_manage_agents`/`n8n_manage_datatable`) or `tags`. Works without the token via the Public API; with it configured, falls back to the official MCP server for team projects when the Public API's licence gate refuses (`teamProjectsEnabled` reports which).\n\n---\n\n## Security & Audit\n\n`n8n_audit_instance` combines n8n's built-in audit (categories `credentials`/`database`/`nodes`/`instance`/`filesystem`) with a custom deep scan (`hardcoded_secrets`, `unauthenticated_webhooks`, `error_handling`, `data_retention`). All parameters optional: `categories`, `includeCustomScan` (default `true`), `customChecks`, `daysAbandonedWorkflow`. Detected secrets are masked (first 6 + last 4 chars). Output is an actionable markdown report — summary table, findings by workflow, and a Remediation Playbook split into auto-fixable / requires-review / requires-user-action.\n\nSee [WORKFLOW_GUIDE.md](WORKFLOW_GUIDE.md) for the two scanning approaches, examples, and remediation types in full.\n\n---\n\n## Self-Help Tools\n\n- `tools_documentation()` — overview of all tools; `tools_documentation({topic, depth: \"full\"})` for a specific tool. Code node guides via topics `javascript_code_node_guide` / `python_code_node_guide`.\n- **AI agent guide** — `tools_documentation({topic: \"ai_agents_guide\", depth: \"full\"})` (no standalone tool); returns architecture, connections, tools, validation, best practices.\n- `n8n_health_check()` — quick check; `n8n_health_check({mode: \"diagnostic\"})` returns status, env vars, tool status, API connectivity. Both modes also return an **`officialMcp`** block — `{configured, endpoint, reachable, toolCount, agentTools}` — the preflight for everything gated on `N8N_MCP_ACCESS_TOKEN`: the agent tools, `n8n_test_workflow`'s routed methods, native version history, the data-table column actions. Read it once before reaching for any of them, rather than discovering the gap through a `NOT_CONFIGURED` envelope mid-task.\n\nSee [OPERATIONS_GUIDE.md](OPERATIONS_GUIDE.md) for examples.\n\n---\n\n## Tool Availability\n\n**Always Available** (no n8n API needed):\n- search_nodes, get_node\n- validate_node, validate_workflow\n- search_templates, get_template\n- tools_documentation (includes the ai_agents_guide topic)\n\n**Requires n8n API** (N8N_API_URL + N8N_API_KEY):\n- n8n_create_workflow\n- n8n_update_partial_workflow, n8n_update_full_workflow\n- n8n_validate_workflow (by ID)\n- n8n_list_workflows, n8n_get_workflow, n8n_delete_workflow\n- n8n_test_workflow\n- n8n_executions\n- n8n_evaluations (reads: n8n 2.30+ with an API key created on 2.30+; run/cancel: n8n 2.32+ with a key created on 2.32+ — older keys lack the testRun scopes)\n- n8n_deploy_template\n- n8n_workflow_versions\n- n8n_autofix_workflow\n- n8n_manage_datatable\n- n8n_manage_folders (folder CRUD: n8n 2.19+, registered Community tier and up; workflow placement via parentFolderId/moveToFolder: n8n 2.32+)\n- n8n_manage_credentials\n- n8n_audit_instance\n- n8n_list_catalog (works without the token; needs it only for the team-project fallback)\n\n**Requires `N8N_MCP_ACCESS_TOKEN`** (a separate token from n8n Settings → Instance-level MCP, in addition to the Public API credentials above):\n- n8n_manage_agents\n- n8n_explore_node_resources\n- n8n_test_workflow with `method: \"prepare\"`/`\"pinned\"`/`\"direct\"` (also needs the workflow's \"Available in MCP\" setting)\n- n8n_workflow_versions with `source: \"native\"` (also needs the workflow's \"Available in MCP\" setting)\n- n8n_manage_datatable with `addColumn`/`deleteColumn`/`renameColumn`\n\nIf API tools unavailable, use templates and validation-only workflows.\n\n---\n\n## Unified Tool Reference\n\n- **`get_node`** — detail levels (`minimal` ~200 tok / `standard` ~1-2K, RECOMMENDED / `full` ~3-8K, sparingly) and modes (`info` default, `docs`, `search_properties` + `propertyQuery`, `versions`, `compare`, `breaking`, `migrations`). Deep dive in [SEARCH_GUIDE.md](SEARCH_GUIDE.md).\n- **`validate_node`** — modes `full` (default, errors/warnings/suggestions) and `minimal` (required-fields check); profiles `minimal`/`runtime` (default, recommended)/`ai-friendly`/`strict`. Deep dive in [VALIDATION_GUIDE.md](VALIDATION_GUIDE.md).\n\n---\n\n## Performance Characteristics\n\n| Tool | Response Time | Payload Size |\n|------|---------------|--------------|\n| search_nodes | <20ms | Small |\n| get_node (standard) | <10ms | ~1-2KB |\n| get_node (full) | <100ms | 3-8KB |\n| validate_node (minimal) | <50ms | Small |\n| validate_node (full) | <100ms | Medium |\n| validate_workflow | 100-500ms | Medium |\n| n8n_manage_folders | 100-500ms | Small |\n| n8n_manage_credentials | 50-500ms | Small-Medium |\n| n8n_audit_instance | 500-5000ms | Large |\n| n8n_create_workflow | 100-500ms | Medium |\n| n8n_update_partial_workflow | 50-200ms | Small |\n| n8n_deploy_template | 200-500ms | Medium |\n\n---\n\n## Best Practices\n\n### Do\n- For simple workflows (<=5 nodes), use MCP tools directly — don't over-engineer the investigation\n- Use `patchNodeField` for surgical edits to Code node content instead of replacing the entire node\n- Use `get_node({detail: \"standard\"})` for most use cases\n- Specify validation profile explicitly (`profile: \"runtime\"`)\n- Use smart parameters (`branch`, `case`) for clarity\n- Include `intent` parameter in workflow updates\n- Follow search → get_node → validate workflow\n- Iterate workflows (avg 56s between edits)\n- Validate after every significant change\n- Use `includeExamples: true` for real configs\n- Use `n8n_deploy_template` for quick starts\n\n### Don't\n- Use `detail: \"full\"` unless necessary (wastes tokens)\n- Forget nodeType prefix (`nodes-base.*`)\n- Skip validation profiles\n- Try to build workflows in one shot (iterate!)\n- Ignore auto-sanitization behavior\n- Use full prefix (`n8n-nodes-base.*`) with search/validate tools\n- Forget to activate workflows after building\n\n---\n\n## Summary\n\n**Most Important**:\n1. Use **get_node** with `detail: \"standard\"` (default) - covers 95% of use cases\n2. nodeType formats differ: `nodes-base.*` (search/validate) vs `n8n-nodes-base.*` (workflows)\n3. Specify **validation profiles** (`runtime` recommended)\n4. Use **smart parameters** (`branch=\"true\"`, `case=0`)\n5. Include **intent parameter** in workflow updates\n6. **Auto-sanitization** runs on ALL nodes during updates\n7. Workflows can be **activated via API** (`activateWorkflow` operation)\n8. Workflows are built **iteratively** (56s avg between edits)\n9. **Data tables** managed with `n8n_manage_datatable` (CRUD + filtering)\n10. **Folders** managed with `n8n_manage_folders`; workflow placement is write-only (verify via folder counts, not the workflow)\n11. **Credentials** managed with `n8n_manage_credentials` (CRUD + schema discovery)\n12. **Security audits** via `n8n_audit_instance` (built-in + custom deep scan)\n13. **AI agent guide** available via `tools_documentation({topic: \"ai_agents_guide\", depth: \"full\"})`\n\n**Common Workflow**:\n1. search_nodes → find node\n2. get_node → understand config\n3. validate_node → check config\n4. n8n_create_workflow → build\n5. n8n_validate_workflow → verify\n6. n8n_update_partial_workflow → iterate\n7. activateWorkflow → go live!\n\nFor details, see:\n- [SEARCH_GUIDE.md](SEARCH_GUIDE.md) - Node discovery\n- [VALIDATION_GUIDE.md](VALIDATION_GUIDE.md) - Configuration validation + common mistakes\n- [WORKFLOW_GUIDE.md](WORKFLOW_GUIDE.md) - Workflow management\n- [OPERATIONS_GUIDE.md](OPERATIONS_GUIDE.md) - Templates, data tables, self-help tools\n\n---\n\n**Related Skills**:\n- n8n Expression Syntax - Write expressions in workflow fields\n- n8n Workflow Patterns - Architectural patterns from templates\n- n8n Validation Expert - Interpret validation errors\n- n8n Node Configuration - Operation-specific requirements\n- n8n Code JavaScript - Write JavaScript in Code nodes\n- n8n Code Python - Write Python in Code nodes\n","data/skills/n8n-expression-syntax/SKILL.md":"---\nname: n8n-expression-syntax\ndescription: Validate n8n expression syntax and fix common errors. Use when writing n8n expressions, using {{}} syntax, accessing $json/$node variables, troubleshooting expression errors, mapping data between nodes, or referencing webhook data in workflows. Use this skill whenever configuring node fields that reference data from previous nodes — expressions are how n8n passes data between nodes, and getting the syntax wrong is the most common source of workflow errors. Also use when asked whether a complex expression hurts performance.\n---\n\n# n8n Expression Syntax\n\nExpert guide for writing correct n8n expressions in workflows.\n\n---\n\n## Expression Format\n\nAll dynamic content in n8n uses **double curly braces**:\n\n```\n{{expression}}\n```\n\n**Examples**:\n```\n✅ {{$json.email}}\n✅ {{$json.body.name}}\n✅ {{$node[\"HTTP Request\"].json.data}}\n❌ $json.email  (no braces - treated as literal text)\n❌ {$json.email}  (single braces - invalid)\n```\n\n---\n\n## Core Variables\n\n### $json - Current Node Output\n\nAccess data from the current node:\n\n```javascript\n{{$json.fieldName}}\n{{$json['field with spaces']}}\n{{$json.nested.property}}\n{{$json.items[0].name}}\n```\n\n### $node - Reference Other Nodes\n\nAccess data from any previous node:\n\n```javascript\n{{$node[\"Node Name\"].json.fieldName}}\n{{$node[\"HTTP Request\"].json.data}}\n{{$node[\"Webhook\"].json.body.email}}\n```\n\n**Important**:\n- Node names **must** be in quotes\n- Node names are **case-sensitive**\n- Must match exact node name from workflow\n\n### $now - Current Timestamp\n\nAccess current date/time:\n\n```javascript\n{{$now}}\n{{$now.toFormat('yyyy-MM-dd')}}\n{{$now.toFormat('HH:mm:ss')}}\n{{$now.plus({days: 7})}}\n```\n\n### $env - Environment Variables\n\nAccess environment variables:\n\n```javascript\n{{$env.API_KEY}}\n{{$env.DATABASE_URL}}\n```\n\n**Warning**: Some n8n instances have `N8N_BLOCK_ENV_ACCESS_IN_NODE` enabled, which blocks `$env` access entirely. If `$env` returns errors, use alternative approaches:\n- Store values in credentials instead\n- Use a Set node with manually entered values\n- Pass values through webhook query parameters\n\n---\n\n## 🚨 CRITICAL: Webhook Data Structure\n\n**Most Common Mistake**: Webhook data is **NOT** at the root!\n\n### Webhook Node Output Structure\n\n```javascript\n{\n  \"headers\": {...},\n  \"params\": {...},\n  \"query\": {...},\n  \"body\": {           // ⚠️ USER DATA IS HERE!\n    \"name\": \"John\",\n    \"email\": \"john@example.com\",\n    \"message\": \"Hello\"\n  }\n}\n```\n\n### Correct Webhook Data Access\n\n```javascript\n❌ WRONG: {{$json.name}}\n❌ WRONG: {{$json.email}}\n\n✅ CORRECT: {{$json.body.name}}\n✅ CORRECT: {{$json.body.email}}\n✅ CORRECT: {{$json.body.message}}\n```\n\n**Why**: Webhook node wraps incoming data under `.body` property to preserve headers, params, and query parameters.\n\n---\n\n## Common Patterns\n\n### Access Nested Fields\n\n```javascript\n// Simple nesting\n{{$json.user.email}}\n\n// Array access\n{{$json.data[0].name}}\n{{$json.items[0].id}}\n\n// Bracket notation for spaces\n{{$json['field name']}}\n{{$json['user data']['first name']}}\n```\n\n### Reference Other Nodes\n\n```javascript\n// Node without spaces\n{{$node[\"Set\"].json.value}}\n\n// Node with spaces (common!)\n{{$node[\"HTTP Request\"].json.data}}\n{{$node[\"Respond to Webhook\"].json.message}}\n\n// Webhook node\n{{$node[\"Webhook\"].json.body.email}}\n```\n\n### Combine Variables\n\n```javascript\n// Concatenation (automatic)\nHello {{$json.body.name}}!\n\n// In URLs\nhttps://api.example.com/users/{{$json.body.user_id}}\n\n// In object properties\n{\n  \"name\": \"={{$json.body.name}}\",\n  \"email\": \"={{$json.body.email}}\"\n}\n```\n\n---\n\n## When NOT to Use Expressions\n\n### ❌ Code Nodes\n\nCode nodes use **direct JavaScript access**, NOT expressions!\n\n```javascript\n// ❌ WRONG in Code node\nconst email = '={{$json.email}}';\nconst name = '{{$json.body.name}}';\n\n// ✅ CORRECT in Code node\nconst email = $json.email;\nconst name = $json.body.name;\n\n// Or using Code node API\nconst email = $input.item.json.email;\nconst allItems = $input.all();\n```\n\n### ❌ Webhook Paths\n\n```javascript\n// ❌ WRONG\npath: \"{{$json.user_id}}/webhook\"\n\n// ✅ CORRECT\npath: \"user-webhook\"  // Static paths only\n```\n\n### ❌ Credential Fields\n\n```javascript\n// ❌ WRONG\napiKey: \"={{$env.API_KEY}}\"\n\n// ✅ CORRECT\nUse n8n credential system, not expressions\n```\n\n---\n\n## The transform gatekeeper\n\nBefore you add any node — or write any code — to transform data, walk this order and stop at the first that fits:\n\n1. **Expression** (`{{ ... }}`) in the consuming field. Property access, method chains (`.map().filter().join()`), ternaries, string building, Luxon date math — if it's \"take A, produce B\" without intermediate variables, it's an expression. This covers most \"just transform this\" cases.\n2. **Arrow-function IIFE inside an Edit Fields field.** When the logic needs intermediate variables, branching, or comments but still operates on one item, wrap it in an immediately-invoked arrow function right in the field value:\n\n   ```\n   ={{ (() => {\n       const items = $json.line_items;\n       const subtotal = items.reduce((sum, it) => sum + it.price * it.qty, 0);\n       const tax = subtotal * 0.08;\n       return (subtotal + tax).toFixed(2);\n   })() }}\n   ```\n\n   The outer `(...)` brackets the function; the trailing `()` invokes it. Drop either and n8n refuses to run. Inside you get the full expression scope (`$json`, `$('Node')`, `$now`, Luxon) plus `const`/`let`, `if`/`switch`, `try`/`catch`, and regex. No `require`, no `await`.\n3. **Code node — last resort.** Only when you need multi-item aggregation across the whole dataset (`$input.all()`), an allowlisted library, or async work.\n\n**Why the order matters.** It's not style — it's readability and performance. The Code node runs in a sandboxed VM with per-invocation setup and value marshaling — a cold-start cost that can reach 500–1000ms before your logic runs. (It amortizes on warm, high-item-count runs, so treat this as the common-case cost, not a universal constant.) The same logic in an expression or Edit Fields IIFE runs in-process in single-digit milliseconds and skips the sandbox entirely. For pure single-item shaping that's a large gap with no functional difference, and it compounds on hot paths like per-request webhooks. The expression also stays visible in the field that uses it, instead of hiding in an upstream node someone has to open to understand. Reach past a stage only when the input or scope genuinely demands it.\n\n## The Set-node antipattern and branch convergence\n\n### Delete Set nodes that feed one consumer\n\nA Set / Edit Fields node whose only job is to extract a value and hand it to **one** downstream node is dead weight. Inline its expression at the consumer instead.\n\n```\n❌  Webhook → Set { customer_id: {{ $json.body.customer_id }} } → Postgres: WHERE id = {{ $json.customer_id }}\n\n✅  Webhook → Postgres: WHERE id = {{ $('Webhook').item.json.body.customer_id }}\n```\n\nThe Set node adds a hop, more canvas clutter, and a refactor hazard, while doing nothing the consumer couldn't do itself. To remove it cleanly with `n8n_update_partial_workflow`: rewire the connection (`removeConnection` from the Set's source-and-target, `addConnection` straight from source to consumer), `patchNodeField` the consumer's expression to reference the original source by node name, then `removeNode` the Set.\n\n**Quick test:** count how many downstream nodes reference each field the Set produces.\n- **0 or 1** → delete, inline at the consumer.\n- **2+** → it may earn its place.\n\n**Legitimate exceptions** — keep the Set when:\n- **2+ consumers** read the same derived value and the derivation is non-trivial (a name aids readability and you compute it once).\n- **It's a sub-workflow's final Return node**, shaping the output contract. Here the \"single consumer\" is every caller, so the Set *is* the API boundary — and with `Include Other Fields: false` it whitelists the output shape so internal scratch fields don't leak.\n- **You're renaming or whitelisting fields** and want that visible in one place rather than spread across consumer expressions.\n\n### Branch convergence: anchor with a NoOp\n\nWhen branches converge (after IF/Switch/Merge), `$json` becomes \"whichever branch fired last\" — non-deterministic, and a silent source of wrong data. Insert a **NoOp** node at the convergence, name it descriptively (`Combine Inputs`), and have downstream nodes reference it by name:\n\n```\nBranch A ──┐\n           ├─→ [NoOp: Combine Inputs] ──→ downstream uses $('Combine Inputs').item.json.x\nBranch B ──┘\n```\n\nThe NoOp survives refactors: inserting a transform later between it and the consumer doesn't break the `$('Combine Inputs')` reference. (If the branches produce *different* shapes, use a Set node instead of a NoOp to normalize both into one shape — see the exceptions above.)\n\nMore broadly in branchy flows, **prefer `$('Node').item.json.x` over deep `$json.x`.** `$json` breaks the moment an intermediate node is inserted or a node clears item context (Aggregate, Code with Run for All, branching merges); the failure is silent and downstream gets the wrong data with no error. A node-name reference is unambiguous regardless of what sits between source and consumer.\n\n---\n\n## Validation Rules\n\n### 1. Always Use {{}}\n\nExpressions **must** be wrapped in double curly braces.\n\n```javascript\n❌ $json.field\n✅ {{$json.field}}\n```\n\n### 2. Use Quotes for Spaces and Special Characters\n\nField or node names with spaces, diacritics, or special characters require **bracket notation**:\n\n```javascript\n❌ {{$json.field name}}\n✅ {{$json['field name']}}\n\n❌ {{$node.HTTP Request.json}}\n✅ {{$node[\"HTTP Request\"].json}}\n\n// Bracket notation is mandatory for keys with special characters\n✅ {{$json['Gross Price w/o shipment']}}\n✅ {{$json['Cena brutto zł']}}\n```\n\n### 3. Match Exact Node Names\n\nNode references are **case-sensitive**:\n\n```javascript\n❌ {{$node[\"http request\"].json}}  // lowercase\n❌ {{$node[\"Http Request\"].json}}  // wrong case\n✅ {{$node[\"HTTP Request\"].json}}  // exact match\n```\n\n### 4. No Nested {{}}\n\nDon't double-wrap expressions:\n\n```javascript\n❌ {{{$json.field}}}\n✅ {{$json.field}}\n```\n\n---\n\n## Common Mistakes\n\nFor complete error catalog with fixes, see [COMMON_MISTAKES.md](COMMON_MISTAKES.md)\n\n### Quick Fixes\n\n| Mistake | Fix |\n|---------|-----|\n| `$json.field` | `{{$json.field}}` |\n| `{{$json.field name}}` | `{{$json['field name']}}` |\n| `{{$node.HTTP Request}}` | `{{$node[\"HTTP Request\"]}}` |\n| `{{{$json.field}}}` | `{{$json.field}}` |\n| `{{$json.name}}` (webhook) | `{{$json.body.name}}` |\n| `'={{$json.email}}'` (Code node) | `$json.email` |\n\n---\n\n## Working Examples\n\nFor real workflow examples, see [EXAMPLES.md](EXAMPLES.md)\n\n### Example 1: Webhook to Slack\n\n**Webhook receives**:\n```json\n{\n  \"body\": {\n    \"name\": \"John Doe\",\n    \"email\": \"john@example.com\",\n    \"message\": \"Hello!\"\n  }\n}\n```\n\n**In Slack node text field**:\n```\nNew form submission!\n\nName: {{$json.body.name}}\nEmail: {{$json.body.email}}\nMessage: {{$json.body.message}}\n```\n\n### Example 2: HTTP Request to Email\n\n**HTTP Request returns**:\n```json\n{\n  \"data\": {\n    \"items\": [\n      {\"name\": \"Product 1\", \"price\": 29.99}\n    ]\n  }\n}\n```\n\n**In Email node** (reference HTTP Request):\n```\nProduct: {{$node[\"HTTP Request\"].json.data.items[0].name}}\nPrice: ${{$node[\"HTTP Request\"].json.data.items[0].price}}\n```\n\n### Example 3: Format Timestamp\n\n```javascript\n// Current date\n{{$now.toFormat('yyyy-MM-dd')}}\n// Result: 2025-10-20\n\n// Time\n{{$now.toFormat('HH:mm:ss')}}\n// Result: 14:30:45\n\n// Full datetime\n{{$now.toFormat('yyyy-MM-dd HH:mm')}}\n// Result: 2025-10-20 14:30\n```\n\n---\n\n## Data Type Handling\n\n### Arrays\n\n```javascript\n// First item\n{{$json.users[0].email}}\n\n// Array length\n{{$json.users.length}}\n\n// Last item\n{{$json.users[$json.users.length - 1].name}}\n```\n\n### Objects\n\n```javascript\n// Dot notation (no spaces)\n{{$json.user.email}}\n\n// Bracket notation (with spaces or dynamic)\n{{$json['user data'].email}}\n```\n\n### Strings\n\n```javascript\n// Concatenation (automatic)\nHello {{$json.name}}!\n\n// String methods\n{{$json.email.toLowerCase()}}\n{{$json.name.toUpperCase()}}\n```\n\n### Numbers\n\n```javascript\n// Direct use\n{{$json.price}}\n\n// Math operations\n{{$json.price * 1.1}}  // Add 10%\n{{$json.quantity + 5}}\n```\n\n---\n\n## Advanced Patterns\n\n### Conditional Content\n\n```javascript\n// Ternary operator\n{{$json.status === 'active' ? 'Active User' : 'Inactive User'}}\n\n// Default values\n{{$json.email || 'no-email@example.com'}}\n```\n\n### Date Manipulation\n\n```javascript\n// Add days\n{{$now.plus({days: 7}).toFormat('yyyy-MM-dd')}}\n\n// Subtract hours\n{{$now.minus({hours: 24}).toISO()}}\n\n// Set specific date\n{{DateTime.fromISO('2025-12-25').toFormat('MMMM dd, yyyy')}}\n```\n\n### String Manipulation\n\n```javascript\n// Substring\n{{$json.email.substring(0, 5)}}\n\n// Replace\n{{$json.message.replace('old', 'new')}}\n\n// Split and join\n{{$json.tags.split(',').join(', ')}}\n```\n\n---\n\n## Performance: expression complexity is (almost) free\n\nA common worry is that a complex `{{ }}` is slow. It isn't — what costs is *how many times* n8n evaluates an expression, not how elaborate each one is.\n\nMeasured on an n8n 2.x instance, an elaborate expression (`sqrt`, `split`, `reduce`, arithmetic) costs the same per item as a trivial `{{ $json.x > 50 }}` — roughly **~0.2 ms/item either way**, because ~90% of that is n8n building the per-item evaluation context, not running your expression.\n\nWhat this means in practice:\n\n- **Don't break a working expression into a chain of nodes for \"speed.\"** Each extra node re-evaluates per item and re-copies all items; one node with one richer expression beats three nodes with simple ones.\n- **An expression (~0.2 ms/item) is ~3× cheaper than a Code node in \"Run Once for Each Item\" mode** (~0.6 ms/item) for the same per-item check — but a Code node in \"Run Once for All Items\" mode is cheaper still (~0.02 ms/item), because it crosses the per-item boundary once instead of N times.\n- This only bites at **thousands of items**; below that it's sub-100 ms. The **n8n Code JavaScript** skill has the full per-item-boundary model.\n\n---\n\n## Debugging Expressions\n\n### Test in Expression Editor\n\n1. Click field with expression\n2. Open expression editor (click \"fx\" icon)\n3. See live preview of result\n4. Check for errors highlighted in red\n\n### Common Error Messages\n\n**\"Cannot read property 'X' of undefined\"**\n→ Parent object doesn't exist\n→ Check your data path\n\n**\"X is not a function\"**\n→ Trying to call method on non-function\n→ Check variable type\n\n**Expression shows as literal text**\n→ Missing {{ }}\n→ Add curly braces\n\n---\n\n## Expression Helpers\n\n### Available Methods\n\n**String**:\n- `.toLowerCase()`, `.toUpperCase()`\n- `.trim()`, `.replace()`, `.substring()`\n- `.split()`, `.includes()`\n\n**Array**:\n- `.length`, `.map()`, `.filter()`\n- `.find()`, `.join()`, `.slice()`\n\n**DateTime** (Luxon):\n- `.toFormat()`, `.toISO()`, `.toLocal()`\n- `.plus()`, `.minus()`, `.set()`\n\n**Number**:\n- `.toFixed()`, `.toString()`\n- Math operations: `+`, `-`, `*`, `/`, `%`\n\n---\n\n## Best Practices\n\n### ✅ Do\n\n- Always use {{ }} for dynamic content\n- Use bracket notation for field names with spaces\n- Reference webhook data from `.body`\n- Use $node for data from other nodes\n- Test expressions in expression editor\n\n### ❌ Don't\n\n- Don't use expressions in Code nodes\n- Don't forget quotes around node names with spaces\n- Don't double-wrap with extra {{ }}\n- Don't assume webhook data is at root (it's under .body!)\n- Don't use expressions in webhook paths or credentials\n\n---\n\n## Related Skills\n\n- **n8n MCP Tools Expert**: Learn how to validate expressions using MCP tools\n- **n8n Workflow Patterns**: See expressions in real workflow examples\n- **n8n Node Configuration**: Understand when expressions are needed\n\n---\n\n## Summary\n\n**Essential Rules**:\n1. Wrap expressions in {{ }}\n2. Webhook data is under `.body`\n3. No {{ }} in Code nodes\n4. Quote node names with spaces\n5. Node names are case-sensitive\n\n**Most Common Mistakes**:\n- Missing {{ }} → Add braces\n- `{{$json.name}}` in webhooks → Use `{{$json.body.name}}`\n- `{{$json.email}}` in Code → Use `$json.email`\n- `{{$node.HTTP Request}}` → Use `{{$node[\"HTTP Request\"]}}`\n\nFor more details, see:\n- [COMMON_MISTAKES.md](COMMON_MISTAKES.md) - Complete error catalog\n- [EXAMPLES.md](EXAMPLES.md) - Real workflow examples\n\n---\n\n**Need Help?** Reference the n8n expression documentation or use n8n-mcp validation tools to check your expressions.\n","data/skills/n8n-code-tool/SKILL.md":"---\nname: n8n-code-tool\ndescription: \"Write JavaScript or Python for the n8n Custom Code Tool (@n8n/n8n-nodes-langchain.toolCode) — the AI-agent-callable tool, NOT the workflow Code node. Use when building a Code Tool attached to an AI Agent, writing code that an LLM will invoke, parsing the `query` input, returning a string result, defining an input schema for structured arguments (specifyInputSchema, jsonSchemaExample, DynamicStructuredTool), or troubleshooting errors like \\\"Wrong output type returned\\\", \\\"No execution data available\\\", \\\"The response property should be a string, but it is an object\\\", \\\"Cannot assign to read only property 'name'\\\", or an AI agent that refuses to call the tool. Covers the critical differences between Code node and Code Tool: return format (string vs `[{json:{...}}]`), unavailability of `$fromAI`/`$input`/`$helpers` in the Code Tool sandbox, naming rules for AI invocation, and when to use `toolWorkflow`/HTTP Request Tool instead.\"\n---\n\n# n8n Custom Code Tool\n\nExpert guidance for writing code inside `@n8n/n8n-nodes-langchain.toolCode` — the tool an AI Agent can invoke, **not** the regular workflow Code node.\n\n---\n\n## ⚠️ This is NOT the Code node\n\nThe Custom Code Tool looks like a Code node in the editor — same JavaScript editor, similar layout — but it is a **completely different node** from a different package with a **different runtime contract**.\n\n| | Code node | Custom Code Tool |\n|---|---|---|\n| **Node type** | `n8n-nodes-base.code` | `@n8n/n8n-nodes-langchain.toolCode` |\n| **Package** | `n8n-nodes-base` | `@n8n/n8n-nodes-langchain` |\n| **Invoked by** | Previous node (workflow flow) | AI Agent (LangChain) |\n| **Input** | `$input.all()` — item stream | `query` — string or object from LLM |\n| **Return** | `[{json: {...}}]` (items array) | **A string** |\n| **`$fromAI()`** | N/A | **Not available** (see Errors) |\n| **HTTP helper** | `this.helpers.httpRequest` (auth helpers blocked) | Not exposed to the tool sandbox |\n| **State** | Per-run execution data | No `getContext`, no `$getWorkflowStaticData` |\n\n**If you treat it like a Code node, it fails.** The rest of this skill covers the Code Tool's actual contract.\n\n---\n\n## Quick Start\n\n### Minimal JavaScript Code Tool\n\n```javascript\n// `query` is whatever the AI sent (a string by default)\nreturn `You asked: ${query}`;\n```\n\n### Minimal Python Code Tool\n\n```python\n# `_query` is whatever the AI sent (a string by default)\nreturn f\"You asked: {_query}\"\n```\n\n### Essential Rules\n\n1. **Return a string.** Numbers are auto-converted. Anything else throws `\"The response property should be a string, but it is an object\"`.\n2. **Input variable is fixed**: `query` (JS), `_query` (Python). You cannot rename it.\n3. **Do NOT use `$fromAI()`** inside the Code Tool sandbox — it throws `\"No execution data available\"`.\n4. **Do NOT use `[{json: {...}}]`** return format — that's for Code nodes. Throws `\"Wrong output type returned\"`.\n5. **Use a descriptive tool name** (letters/numbers/underscores, v1.1+). The agent calls the tool by its name.\n6. **Write a precise description** — the LLM decides whether to invoke the tool based on it.\n\n---\n\n## The Two Input Modes\n\nThe Code Tool has two input shapes, controlled by `specifyInputSchema`:\n\n### Mode 1: Unstructured (default, `specifyInputSchema: false`)\n\nThe AI passes **a single string** as `query`. If you need multiple fields, the AI has to stuff them into that one string and you parse them out. In practice, LLMs will happily pass a JSON string if your description tells them to.\n\n```javascript\n// Parse a JSON string the AI sent\nlet params;\ntry {\n  params = typeof query === 'string' ? JSON.parse(query) : query;\n} catch (e) {\n  throw new Error('Expected a JSON object. Parser said: ' + e.message);\n}\nconst price = Number(params.price);\nconst months = Number(params.months);\n// ...\nreturn JSON.stringify({ monthly_payment: /* ... */ });\n```\n\n**Pros**: simplest to set up, one field to describe.\n**Cons**: no schema validation — if the LLM forgets a field, the tool throws at runtime.\n\n**Best for**: quick prototypes, tools with one natural input (a question, a URL, a text blob).\n\n### Mode 2: Structured (`specifyInputSchema: true`)\n\nThe tool becomes a LangChain `DynamicStructuredTool`. The LLM sees a typed argument schema and passes a **validated object** as `query`. You access fields directly.\n\n```javascript\n// query is now an object matching your schema\nconst price = query.price;\nconst months = query.months;\nconst residual_percent = query.residual_percent;\n\nconst monthly = computeAnnuity(price, months, residual_percent);\nreturn JSON.stringify({ monthly_payment: monthly });\n```\n\nSchema is defined via either:\n- `schemaType: \"fromJson\"` + `jsonSchemaExample` (n8n v≥1.3) — paste an example JSON, n8n infers the schema\n- `schemaType: \"manual\"` + `inputSchema` — write a full JSON Schema yourself\n\n**Pros**: LLM gets type hints, invalid calls rejected before your code runs, cleaner code.\n**Cons**: a little more setup; requires n8n version with schema support.\n\n**Best for**: production tools with multiple typed parameters (calculators, API wrappers, anything with numeric fields the LLM tends to stringify).\n\n**See**: [INPUT_SCHEMA.md](INPUT_SCHEMA.md) for complete schema setup.\n\n---\n\n## Return Format\n\n**The return value must be a string.** The LLM reads it as the tool's observation.\n\n```javascript\n// ✅ String\nreturn \"42\";\n\n// ✅ Number (auto-converted to string by n8n)\nreturn 42;\n\n// ✅ JSON-encoded structured result (recommended for rich output)\nreturn JSON.stringify({ result: 42, currency: \"SEK\" });\n\n// ❌ Raw object → \"The response property should be a string, but it is an object\"\nreturn { result: 42 };\n\n// ❌ Workflow item format → \"Wrong output type returned\"\nreturn [{ json: { result: 42 } }];\n\n// ❌ Array → \"The response property should be a string, but it is an object\"\nreturn [1, 2, 3];\n```\n\n### Best practice: JSON-stringify structured results\n\nWhen your tool has more than a trivial scalar output, return a JSON string:\n\n```javascript\nreturn JSON.stringify({\n  monthly_payment_sek: 5405,\n  loan_amount: 351920,\n  total_cost_of_credit: 63295\n});\n```\n\nThe LLM parses JSON reliably and can pick the fields it needs to present to the user.\n\n### Error handling: the agent reads your failures\n\nErrors don't just stop the workflow — they go back to the LLM, which usually corrects its call and retries. Use that:\n\n```javascript\n// Option A: throw — n8n surfaces the message to the agent\nif (!isFinite(price)) throw new Error('price must be a number, e.g. 439900');\n\n// Option B: return an error string — agent reads it like any tool result\nif (!isFinite(price)) return JSON.stringify({ error: 'price must be a number, e.g. 439900' });\n```\n\nEither way, write error messages **for the LLM**: state what was wrong and what a valid call looks like. A bare `throw new Error('invalid input')` wastes the retry; an instructive message usually fixes the next call.\n\n---\n\n## Tool Name and Description\n\nThese fields are NOT documentation — they are the **tool contract the LLM sees**. Treat them as prompt engineering.\n\n### Name\n- Must match `[A-Za-z0-9_]+` (v1.1+). No spaces, no hyphens, no emoji.\n- Use a verb-y descriptive name: `calculate_car_loan`, `get_weather`, `search_orders`.\n- The agent calls the tool by this name. `Code Tool` (the default) is useless — the agent won't know when to call it.\n\n### Description\n- Explain **when** to use it and **what** to send.\n- If unstructured mode, **include an example of the JSON string** the LLM should send.\n- If structured mode, the schema speaks for itself — just describe purpose.\n\n**Unstructured example (JSON-in-string pattern):**\n```\nDeterministiskt beräknar månadskostnad för billån. Anropa med EN JSON-sträng:\n{\"price\":439900,\"down_payment\":87980,\"interest_rate\":6.95,\"months\":36,\"residual_percent\":50}\nFält: price (SEK), down_payment (SEK), interest_rate (% per år), months, residual_percent (0-99).\n```\n\n**Structured example (schema-defined):**\n```\nDeterministically computes the monthly car-loan payment given price, down payment, \nannual interest rate, term, and residual percent. Use whenever the user asks for \nmonthly cost, total credit cost, or loan breakdown.\n```\n\n---\n\n## Top Errors and Fixes\n\n### Error 1: `\"There was an error: 'Cannot assign to read only property \\\"name\\\" of object: Error: No execution data available'\"`\n\n**Cause**: you called `$fromAI()` inside the Code Tool sandbox.\n\n**Fix**: `$fromAI()` is a helper for **other** tool-enabled nodes (HTTP Request Tool, SendGrid Tool, `toolWorkflow`, etc.) — it's not exposed inside `toolCode`. Read the AI's input from `query` directly (or use `specifyInputSchema` for structured fields).\n\n### Error 2: `\"Wrong output type returned\"`\n\n**Cause**: you returned a workflow-style array like `[{ json: { ... } }]`. That's the Code **node** contract, not the Code **Tool** contract.\n\n**Fix**: return a string. For structured data, `return JSON.stringify(output)`.\n\n### Error 3: `\"The response property should be a string, but it is an object\"`\n\n**Cause**: you returned a plain object or array.\n\n**Fix**: `JSON.stringify()` the result, or coerce to a string.\n\n### Error 4: AI never calls the tool\n\n**Cause**: tool name is generic (`Code Tool`, `My Tool`) or description doesn't clearly state when to use it.\n\n**Fix**: rename to a verb-y name (`calculate_car_loan`), and rewrite the description to explicitly state the trigger conditions (e.g. \"Use this whenever the user asks about monthly cost\").\n\n### Error 5: AI sends garbage into `query`\n\n**Cause**: unstructured tool with a vague description. The LLM guesses at the format.\n\n**Fix**: either (a) include a concrete JSON example in the description, or (b) switch to `specifyInputSchema: true` so the LLM gets a typed schema.\n\n**See**: [ERROR_PATTERNS.md](ERROR_PATTERNS.md) for full catalog with reproductions.\n\n---\n\n## What's NOT Available in the Sandbox\n\nThe Code Tool sandbox is **narrower** than the Code node sandbox. Don't assume helpers carry over:\n\n| Helper | Code node | Code Tool |\n|---|---|---|\n| `$input.all()`, `$input.first()`, `$input.item` | ✅ | ❌ |\n| `$node[\"NodeName\"]` | ✅ | ❌ |\n| `$json`, `$binary` | ✅ | ❌ |\n| `$fromAI()` | ❌ | ❌ (despite sitting next to an AI agent) |\n| `this.helpers.httpRequest()` | ✅ | ❌ |\n| `DateTime` (Luxon) | ✅ | ✅ (standard in JS sandbox) |\n| `$jmespath()` | ✅ | ❌ |\n| `this.getContext(...)` | ✅ | ❌ |\n| `$getWorkflowStaticData(...)` | ✅ | ❌ |\n\n**Implication**: the Code Tool is for **pure computation**. If you need an HTTP call, an API lookup, or cross-invocation state, use a different tool node:\n- HTTP Request Tool for external API calls\n- `toolWorkflow` (Call Sub-workflow Tool) for multi-step logic with access to the full Code node sandbox\n- MCP / database tools for persistent state\n\n---\n\n## When to Use Code Tool vs Alternatives\n\nUse **Code Tool** when:\n- ✅ Pure deterministic computation (math, parsing, formatting, validation)\n- ✅ Lightweight transformations the LLM shouldn't do itself (precision math, regex)\n- ✅ You want the code inline in the workflow, not in a separate sub-workflow\n\nUse **`toolWorkflow`** (Call Sub-workflow Tool) when:\n- ✅ You need multiple parameters with clean `$fromAI()` typing\n- ✅ You need access to `this.helpers`, credentials, or other nodes\n- ✅ Logic is reusable across agents\n- ✅ You want structured typed inputs WITHOUT writing a JSON Schema\n\nUse **HTTP Request Tool** when:\n- ✅ The tool is fundamentally a single API call\n- ✅ You want per-parameter `$fromAI()` bindings in URL/query/body\n\n**Rule of thumb**: if you find yourself wanting `$fromAI()`, you probably want `toolWorkflow` instead of `toolCode`.\n\n---\n\n## Complete Working Example\n\nA production calculator tool (unstructured, JSON-in-string pattern):\n\n```json\n{\n  \"parameters\": {\n    \"name\": \"calculate_car_loan\",\n    \"description\": \"Computes monthly car-loan payment using an annuity formula with residual/balloon. Call with a single JSON string. Example: {\\\"price\\\":439900,\\\"down_payment\\\":87980,\\\"interest_rate\\\":6.95,\\\"months\\\":36,\\\"residual_percent\\\":50,\\\"setup_fee\\\":695,\\\"monthly_admin_fee\\\":59}. Required: price, down_payment, interest_rate, months, residual_percent. Optional: setup_fee, monthly_admin_fee (default 0).\",\n    \"language\": \"javaScript\",\n    \"jsCode\": \"let params;\\ntry {\\n  params = typeof query === 'string' ? JSON.parse(query) : query;\\n} catch (e) {\\n  throw new Error('Invalid JSON: ' + e.message);\\n}\\n\\nconst price           = Number(params.price);\\nconst down_payment    = Number(params.down_payment);\\nconst interest_rate   = Number(params.interest_rate);\\nconst months          = Number(params.months);\\nconst residual_percent= Number(params.residual_percent);\\nconst setup_fee       = Number(params.setup_fee ?? 0) || 0;\\nconst monthly_admin_fee = Number(params.monthly_admin_fee ?? 0) || 0;\\n\\nif (!isFinite(price) || price <= 0) throw new Error('price must be > 0');\\nif (down_payment < 0 || down_payment >= price) throw new Error('down_payment must be in [0, price)');\\n\\nconst principal = price - down_payment;\\nconst residual  = price * (residual_percent / 100);\\nconst r = interest_rate / 100 / 12;\\nconst growth = Math.pow(1 + r, months);\\nconst base = r === 0\\n  ? (principal - residual) / months\\n  : (principal - residual / growth) * r / (1 - 1 / growth);\\nconst monthly_payment = base + monthly_admin_fee;\\n\\nreturn JSON.stringify({\\n  monthly_payment_sek: Math.round(monthly_payment),\\n  loan_amount: Math.round(principal),\\n  residual_value_sek: Math.round(residual),\\n  total_cost_of_credit: Math.round(monthly_payment * months + residual + setup_fee - principal)\\n});\"\n  },\n  \"type\": \"@n8n/n8n-nodes-langchain.toolCode\",\n  \"typeVersion\": 1.3,\n  \"name\": \"calculate_car_loan\"\n}\n```\n\nWire it into an AI Agent via the `ai_tool` connection type.\n\n---\n\n## Integration with Other Skills\n\n**n8n-code-javascript**: the Code **node** skill. Most JavaScript patterns (arrays, map/filter, DateTime) transfer — but I/O contract is different. Don't copy data-access code.\n\n**n8n-node-configuration**: `specifyInputSchema` is a classic displayOptions-driven conditional field. Use `get_node({detail: \"standard\"})` on `@n8n/n8n-nodes-langchain.toolCode` to see schema-related properties.\n\n**n8n-workflow-patterns**: Code Tool sits inside the \"AI Agent with tools\" pattern. An agent typically has several tools; Code Tool is the \"local compute\" option.\n\n**n8n-validation-expert**: the three Code Tool errors listed above have clear signatures — if validation surfaces \"Wrong output type returned\", you know to switch from array-of-items to a string.\n\n---\n\n## Quick Reference Checklist\n\nBefore deploying a Code Tool:\n\n- [ ] **Node type** is `@n8n/n8n-nodes-langchain.toolCode` (not `nodes-base.code`)\n- [ ] **Tool name** is descriptive, verb-y, snake_case (e.g. `calculate_car_loan`)\n- [ ] **Description** states when to use the tool and (if unstructured) shows a JSON example\n- [ ] **Input** read from `query` (JS) or `_query` (Python)\n- [ ] **No `$fromAI()`** in the code body\n- [ ] **No `$input` / `$json` / `$helpers`** — those aren't in the sandbox\n- [ ] **Return** is a string (use `JSON.stringify()` for structured output)\n- [ ] **Wired** into an AI Agent via `ai_tool` connection\n- [ ] **Tested** with the exact kind of input the LLM will send (JSON in a string, or schema-validated object)\n\n---\n\n## Additional Resources\n\n- [INPUT_SCHEMA.md](INPUT_SCHEMA.md) — structured input (DynamicStructuredTool) in depth\n- [ERROR_PATTERNS.md](ERROR_PATTERNS.md) — full error catalog with causes and fixes\n\n### Official sources\n- [n8n Custom Code Tool docs](https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.toolcode/)\n- [ToolCode source](https://github.com/n8n-io/n8n/blob/master/packages/%40n8n/nodes-langchain/nodes/tools/ToolCode/ToolCode.node.ts) — the sandbox contract\n- [LangChain tool docs](https://js.langchain.com/docs/modules/agents/tools/) — DynamicTool / DynamicStructuredTool\n\n---\n\n**Remember**: the Code Tool is a LangChain tool wearing a Code-node UI. Contract is: **string in, string out**. Everything else follows from that.\n","data/skills/n8n-multi-instance/SKILL.md":"---\nname: n8n-multi-instance\ndescription: Use when an n8n-mcp account targets more than one n8n instance — i.e. the `n8n_instances` tool is available, the user mentions multiple n8n instances or environments (prod vs staging, several teams or clients), a workflow / datatable / credential / execution call returns an unexpected NOT_FOUND or reads data you don't recognize, or a credential create/update/delete is refused with an `INSTANCE_AMBIGUOUS` error. Covers choosing and switching which instance this MCP session targets, verifying the target before high-stakes work — credential writes above all — and recovering from misroutes and ambiguous-write fail-closes. Always consult this skill before operating on a specific instance, before any credential create/update/delete on a multi-instance account, or when a call hits the wrong/empty data or an `INSTANCE_AMBIGUOUS` error.\n---\n\n# Working with multiple n8n instances over MCP\n\nWhen the `n8n_instances` tool is available, the user has **multi-instance mode** on: one MCP\nconnection can reach several n8n instances (e.g. `prod`, `staging`, or one per client/team).\nEvery other n8n tool (`n8n_get_workflow`, `n8n_list_workflows`, `n8n_update_partial_workflow`,\n`n8n_manage_datatable`, `n8n_manage_credentials`, `n8n_executions`, `n8n_test_workflow`, …) runs\nagainst **whichever instance this session is currently targeting**. There is no per-call instance\nargument: you change the target only by switching. Target the wrong instance and a read returns the\nwrong data and a write lands in the wrong place — usually with **no error** (the one exception is an\nambiguous credential write, which fails closed; see below). So target deliberately.\n\nIf the `n8n_instances` tool is **not** present, the account is single-instance: ignore this skill\nand use the n8n tools directly.\n\n## Golden rules\n\nSix rules. Each prevents a class of silent misroute.\n\n1. **Discover first.** Call `n8n_instances({mode:\"list\"})` before acting so you know the instance\n   names and which one is `current`.\n2. **Switch by name to your target** before doing work on a non-default instance:\n   `n8n_instances({mode:\"switch\", name:\"<instance name>\"})`. The match is case-insensitive.\n3. **Switch in its own turn.** Never put a `switch` and a dependent operation in the **same\n   parallel tool-call batch**. Calls in one batch have no guaranteed order, so the dependent call\n   can be resolved against the *previous* instance before the switch's session state is visible.\n   Switch, let it return, *then* operate.\n4. **Verify before high-stakes ops.** Immediately before creating/updating/deleting **credentials**\n   (and before destructive workflow edits), confirm `current` is the instance you intend — primary\n   check is `n8n_instances({mode:\"list\"})`. The system fail-closes only the *ambiguous* credential\n   case (rule 6); an explicit switch to the **wrong** instance still writes there silently, so this\n   check is on you.\n5. **An unexpected `NOT_FOUND` is almost always a wrong-instance misroute, not a deletion.** Don't\n   recreate the object. Re-check the current instance and retry (see Recovery).\n6. **On `INSTANCE_AMBIGUOUS`, switch on *this* session, then retry.** The system is refusing to\n   write a secret because this session never picked a target itself. Comply — run `switch` here to\n   confirm the instance, then retry the write. Don't work around it or retry blindly.\n\n## Core workflow\n\n```\n1. n8n_instances({mode:\"list\"})                      # see available[] + current + default\n2. n8n_instances({mode:\"switch\", name:\"prod\"})       # bind THIS session to \"prod\"\n   → returns { previous, current }; confirm current.name == \"prod\"\n3. (do your work) n8n_list_workflows / n8n_get_workflow / n8n_manage_datatable / ...\n4. Before a credential write or a delete:\n   n8n_instances({mode:\"list\"})  → re-confirm current, THEN n8n_manage_credentials({action:\"create\", ...})\n```\n\nTo move to another instance, just `switch` again. The whole session follows the switch.\n\n## The `n8n_instances` tool\n\nTwo modes (`mode` is required and enum-validated):\n\n- `{mode:\"list\"}` → `{ current, default, available }`, no side effects.\n  - `current` and `default` are each one instance `{ id, name, url, isDefault }` (or `null`).\n  - `available` is every instance, each with an extra `isCurrent` boolean. Match by **`name`**;\n    never hard-code `id`.\n- `{mode:\"switch\", name:\"<name>\"}` → `{ previous, current }`, and binds this session to the named\n  instance. `name` is case-insensitive.\n\n### Error envelope (from the `n8n_instances` tool)\n\nEvery error returns `{ error: \"<CODE>\", message, … }`. The ones you'll actually hit:\n\n| Code | When | What to do |\n|---|---|---|\n| `UNKNOWN_INSTANCE` | `name` matches no instance | Pick a name from the `available` list in the error payload and retry. |\n| `NAME_REQUIRED` | `switch` with no `name` | Re-call with a `name` (the error lists the valid ones in `available`). |\n| `MULTI_INSTANCE_DISABLED` | multi-instance mode is off | There's nothing to switch; use the n8n tools directly. The user can enable it at the n8n-mcp dashboard. |\n| `NO_SESSION` | the request has **neither** an MCP session id **nor** a credential id | A selection has nowhere to land. Reconnect / initialize a session, then switch. |\n| `UNKNOWN_MODE` | `mode` wasn't `list`/`switch` | Use `list` or `switch`. |\n| `INVALID_CONTEXT` | server-side metadata missing | A server bug, not your input — report it. |\n\n> Instance names can never be `default`, `current`, `list`, or `switch` (reserved), so you'll never\n> see an instance literally named after a mode or field.\n\n### `INSTANCE_AMBIGUOUS` (from the credential-write path, not the tool)\n\nA separate, higher-stakes error. It is **not** returned by `n8n_instances` — it's returned by the\nserver when you call `n8n_manage_credentials` to **create/update/delete** a credential and the target\ninstance is ambiguous: this session never switched on its own but inherited a switch made elsewhere\n(a fan-out / reconnect), pointing at a **non-default** instance. Rather than risk writing a secret to\nthe wrong instance, the server **blocks the write** (it never reaches n8n, no quota is charged) and\nreturns:\n\n```json\n{\n  \"error\": \"INSTANCE_AMBIGUOUS\",\n  \"message\": \"… the session issuing this request never switched there itself … Re-run n8n_instances({mode:\\\"switch\\\", name:\\\"…\\\"}) on this session to confirm the target …\",\n  \"lastSelected\": { \"id\": \"…\", \"name\": \"…\" },\n  \"default\":      { \"id\": \"…\", \"name\": \"…\" }\n}\n```\n\n**Fix:** decide which instance you actually want (`lastSelected` is the inherited switch, `default`\nis the account default), run `n8n_instances({mode:\"switch\", name:\"…\"})` on **this** session, then\nretry the write. See rule 6.\n\n## How targeting behaves (mental model)\n\n- A `switch` **binds this session** to the chosen instance. The binding **persists for the rest of\n  the session and survives reconnects, idle, and backend deploys** (~24h, the MCP session lifetime)\n  — you should not need to re-switch before every call.\n- Other sessions / terminals are **independent**: switching here does not move them.\n- One session targets **one instance at a time**. There is no per-call instance argument; you\n  change the target only via `switch`.\n- **Reads and non-credential writes** route to the currently-selected instance, silently — a\n  misroute produces wrong data or a `NOT_FOUND`, not an error.\n- **Credential writes are the one guarded case.** They route the same way, except the server\n  fail-closes the *ambiguous* state (a session that never switched, recovered onto a non-default\n  instance) with `INSTANCE_AMBIGUOUS`. This is a safety net, not a substitute for rule 4: an\n  explicit switch to the wrong instance still writes there.\n- **If your selected instance is deleted** (the user removes it mid-session), the next call silently\n  falls back to your **default** instance — no error. So default's data appearing where you expected\n  another instance's can look like \"my data vanished.\" Re-list to see where you are.\n\n## Recovery playbook\n\n| Symptom | What it usually means | Do this |\n|---|---|---|\n| `INSTANCE_AMBIGUOUS` on a credential create/update/delete | This session never switched itself; the system won't guess which instance to write the secret to | Run `n8n_instances({mode:\"switch\", name:\"<target>\"})` on this session (the error names `lastSelected` and `default` — pick the one you want), then retry the write. Never retry blindly. |\n| `NOT_FOUND` for a workflow/datatable/credential you **know exists** | You're pointed at the wrong instance — **not** that it was deleted | `n8n_instances({mode:\"list\"})` → check `current`. If it's not your target, `switch` and retry. **Do not recreate the object.** |\n| A read returns **empty or unfamiliar** data | Wrong-instance read, or a silent fallback to `default` after your instance was deleted | `n8n_instances({mode:\"list\"})`, confirm `current`, switch if needed, re-read before drawing conclusions. |\n| `UNKNOWN_INSTANCE` on `switch` | The `name` is wrong (typo, or you guessed) | Read the `available` names in the error and switch to one of those. Names are case-insensitive. |\n| `n8n_health_check` reports an `instanceName` you didn't expect | This session is on a different instance than you think | `switch` to the intended instance, then proceed. |\n| Repeated misroutes within one turn | You batched a `switch` with dependent work | Split them: `switch` alone, await the result, then operate one logical step at a time. |\n\nAfter any recovery switch, sanity-check with `n8n_instances({mode:\"list\"})` (read `current`) as the\nprimary signal. `n8n_health_check` also returns the resolved instance under `details.instanceName`,\nbut it can be absent on some paths (legacy/chat), so treat it as a secondary confirmation.\n\n## Credential operations (highest stakes)\n\nCredentials hold live secrets, and a misrouted credential write puts a secret on the **wrong\ninstance**. The server protects the **ambiguous** case automatically — if this session never picked\na target and inherited a switch to a non-default instance, the write fails closed with\n`INSTANCE_AMBIGUOUS` (rule 6) and never reaches n8n. But that net is narrow: a credential write on a\nsession that **did** switch goes through to whatever instance it switched to, with no second\nguess. So:\n\n- **Verify `current` immediately before** `n8n_manage_credentials` create/update/delete — call\n  `n8n_instances({mode:\"list\"})` in the same short sequence, not 10 steps earlier where a later\n  switch could have moved you.\n- **On `INSTANCE_AMBIGUOUS`**, switch on this session to confirm the target, then retry — don't\n  work around it.\n- Credential **reads** (`action:\"list\"`/`\"get\"`/`\"getSchema\"`) are not gated and don't write a\n  secret, but a read off the wrong instance returns the wrong schema or list — so still verify\n  `current` if the result looks wrong.\n- For the `n8n_manage_credentials` tool itself (CRUD shapes, `getSchema` discovery, never inlining\n  secrets into text fields), see `n8n-mcp-tools-expert`.\n\n## Common multi-instance task: copy something between instances\n\nTo recreate a credential or workflow from instance A on instance B:\n\n```\n1. switch → A;  read the source (n8n_manage_credentials get / n8n_get_workflow)\n2. switch → B   (its own call — never batched with the create below)\n3. n8n_instances({mode:\"list\"})  → confirm current == B\n4. create on B  (n8n_manage_credentials create / n8n_create_workflow)\n```\n\nDo each instance's steps in its own turn; never overlap `switch → B` with the create-on-B call\n(rule 3), and switch explicitly on this session before the credential write so it isn't ambiguous\n(rules 4 and 6).\n\n## Quick reference\n\n- See instances + where you are: `n8n_instances({mode:\"list\"})` → `{ current, default, available }`\n- Change target: `n8n_instances({mode:\"switch\", name:\"<name>\"})` — its own turn, then operate\n- Confirm target: `current` from `list` (primary); `details.instanceName` from `n8n_health_check` (secondary, may be absent)\n- `UNKNOWN_INSTANCE` → switch to a name from the error's `available` list, then retry\n- `INSTANCE_AMBIGUOUS` (credential write) → `switch` on this session to confirm the target, then retry\n- Unexpected `NOT_FOUND` → verify the instance, switch, retry; **do not recreate**\n- Before credential writes → re-`list`, confirm `current`, then write (the fail-close only covers the ambiguous case)\n\n## Integration with other skills\n\n- **n8n-mcp-tools-expert** — owns `n8n_manage_credentials` (CRUD + `getSchema`) and the rule that\n  secrets go through the credential system, never text fields. This skill adds the \"which instance?\"\n  layer on top.\n- **using-n8n-mcp-skills** — the router; consult it for which skill owns a given build step.\n","data/skills/n8n-node-configuration/SKILL.md":"---\nname: n8n-node-configuration\ndescription: Operation-aware node configuration guidance. Use when configuring nodes, understanding property dependencies, determining required fields, choosing between get_node detail levels, or learning common configuration patterns by node type. Always use this skill when setting up node parameters — it explains which fields are required for each operation, how displayOptions control field visibility, and when to use patchNodeField for surgical edits vs full node updates.\n---\n\n# n8n Node Configuration\n\nExpert guidance for operation-aware node configuration with property dependencies.\n\n---\n\n## Configuration Philosophy\n\n**Progressive disclosure**: Start minimal, add complexity as needed\n\nConfiguration best practices:\n- `get_node` with `detail: \"standard\"` is the most used discovery pattern\n- 56 seconds average between configuration edits\n- Covers 95% of use cases with 1-2K tokens response\n\n**Key insight**: Most configurations need only standard detail, not full schema!\n\n---\n\n## Core Concepts\n\n### 1. Operation-Aware Configuration\n\n**Not all fields are always required** - it depends on operation!\n\n**Example**: Slack node\n```javascript\n// For operation='post'\n{\n  \"resource\": \"message\",\n  \"operation\": \"post\",\n  \"channel\": \"#general\",  // Required for post\n  \"text\": \"Hello!\"        // Required for post\n}\n\n// For operation='update'\n{\n  \"resource\": \"message\",\n  \"operation\": \"update\",\n  \"messageId\": \"123\",     // Required for update (different!)\n  \"text\": \"Updated!\"      // Required for update\n  // channel NOT required for update\n}\n```\n\n**Key**: Resource + operation determine which fields are required!\n\n### 2. Property Dependencies\n\n**Fields appear/disappear based on other field values**\n\n**Example**: HTTP Request node\n```javascript\n// When method='GET'\n{\n  \"method\": \"GET\",\n  \"url\": \"https://api.example.com\"\n  // sendBody not shown (GET doesn't have body)\n}\n\n// When method='POST'\n{\n  \"method\": \"POST\",\n  \"url\": \"https://api.example.com\",\n  \"sendBody\": true,       // Now visible!\n  \"body\": {               // Required when sendBody=true\n    \"contentType\": \"json\",\n    \"content\": {...}\n  }\n}\n```\n\n**Mechanism**: displayOptions control field visibility\n\n### 3. Progressive Discovery\n\n**Use the right detail level**:\n\n1. **get_node({detail: \"standard\"})** - DEFAULT\n   - Quick overview (~1-2K tokens)\n   - Required fields + common options\n   - **Use first** - covers 95% of needs\n\n2. **get_node({mode: \"search_properties\", propertyQuery: \"...\"})** (for finding specific fields)\n   - Find properties by name\n   - Use when looking for auth, body, headers, etc.\n\n3. **get_node({detail: \"full\"})** (complete schema)\n   - All properties (~3-8K tokens)\n   - Use only when standard detail is insufficient\n\n---\n\n## Configuration Workflow\n\n### Standard Process\n\n1. Identify node type and operation.\n2. Use `get_node` (standard detail is default).\n3. Configure required fields.\n4. Validate configuration.\n5. If a field is unclear → `get_node({mode: \"search_properties\"})`.\n6. Add optional fields as needed.\n7. Validate again.\n8. Deploy.\n\n### Example: Configuring HTTP Request\n\nThe validate-driven loop in practice: start minimal (`method`, `url`, `authentication`), then let each `validate_node` error surface the next required field (`sendBody` for POST → `body` when `sendBody=true`) until valid. Full step-by-step walkthrough in **[OPERATION_PATTERNS.md](OPERATION_PATTERNS.md#worked-example-configuring-http-request-step-by-step)**.\n\n---\n\n## get_node Detail Levels\n\n### Standard Detail (DEFAULT - Use This!)\n\n**✅ Starting configuration**\n```javascript\nget_node({\n  nodeType: \"nodes-base.slack\"\n});\n// detail=\"standard\" is the default\n```\n\n**Returns** (~1-2K tokens):\n- Required fields\n- Common options\n- Operation list\n- Metadata\n\n**Use**: 95% of configuration needs\n\n### Full Detail (Use Sparingly)\n\n**✅ When standard isn't enough**\n```javascript\nget_node({\n  nodeType: \"nodes-base.slack\",\n  detail: \"full\"\n});\n```\n\n**Returns** (~3-8K tokens):\n- Complete schema\n- All properties\n- All nested options\n\n**Warning**: Large response, use only when standard insufficient\n\n### Search Properties Mode\n\n**✅ Looking for specific field**\n```javascript\nget_node({\n  nodeType: \"nodes-base.httpRequest\",\n  mode: \"search_properties\",\n  propertyQuery: \"auth\"\n});\n```\n\n**Use**: Find authentication, headers, body fields, etc.\n\n### Decision Tree\n\n1. Starting a new node config → `get_node` (standard).\n2. Standard has what you need → configure with it. Otherwise continue.\n3. Looking for a specific field → `search_properties` mode. Otherwise continue.\n4. Still need more → `get_node({detail: \"full\"})`.\n\n**Dynamic properties**: when `standard` detail marks a property with `dynamicOptions: {methodName, methodType, dependsOn}`, its real values come from a live `loadOptions`/`listSearch` method, not from bundled docs — don't guess an ID for it. Resolve it with `n8n_explore_node_resources` (needs `N8N_MCP_ACCESS_TOKEN`, n8n 2.34+) and put the returned `value` in the config; `name` is display text only.\n\nAll six parameters are required and none are inferred from each other:\n\n```javascript\nn8n_explore_node_resources({\n  nodeType: \"n8n-nodes-base.googleSheets\",  // LONG form\n  version: 4.5,                              // the node typeVersion the method belongs to\n  methodName: \"getSheets\",                   // copied verbatim from dynamicOptions\n  methodType: \"listSearch\",                  // \"listSearch\" for resource locators, \"loadOptions\" for plain dropdowns\n  credentialType: \"googleSheetsOAuth2Api\",\n  credentialId: \"c2\",                        // from n8n_manage_credentials({action: \"list\"})\n  currentNodeParameters: {                   // whatever dependsOn names, in its real shape\n    documentId: {__rl: true, mode: \"id\", value: \"1AbC…\"}\n  }\n})\n```\n\n`dependsOn` names the parameters the method needs already chosen — pass them in `currentNodeParameters`, keeping resource-locator values in their `{__rl: true, mode, value}` shape, or the method returns nothing useful. `methodName` is case-sensitive and specific to the `nodeType` + `version` pair; a mismatch returns `OFFICIAL_MCP_ERROR` rather than an empty list.\n\n---\n\n## Property Dependencies Deep Dive\n\nFields have `displayOptions` visibility rules: `show`/`hide` blocks where multiple conditions are AND'd and multiple values are OR'd (e.g. `body` shows when `sendBody=true` AND `method IN (POST, PUT, PATCH)`). The three recurring patterns are the boolean toggle (sendBody → body), the operation switch (post vs update show different fields), and type selection (string vs boolean conditions). To find what controls a field, use `get_node({mode: \"search_properties\", propertyQuery: \"...\"})` or `get_node({detail: \"full\"})` — especially when validation flags a field you don't see.\n\nMechanism details, all four dependency patterns, complex flows, nested dependencies, and troubleshooting are in **[DEPENDENCIES.md](DEPENDENCIES.md)** (quick-reference recap under [Quick Reference: displayOptions and Common Dependency Patterns](DEPENDENCIES.md#quick-reference-displayoptions-and-common-dependency-patterns)).\n\n---\n\n## Common Node Patterns\n\n### Pattern 1: Resource/Operation Nodes\n\n**Examples**: Slack, Google Sheets, Airtable\n\n**Structure**:\n```javascript\n{\n  \"resource\": \"<entity>\",      // What type of thing\n  \"operation\": \"<action>\",     // What to do with it\n  // ... operation-specific fields\n}\n```\n\n**How to configure**:\n1. Choose resource\n2. Choose operation\n3. Use get_node to see operation-specific requirements\n4. Configure required fields\n\n### Pattern 2: HTTP-Based Nodes\n\n**Examples**: HTTP Request, Webhook\n\n**Structure**:\n```javascript\n{\n  \"method\": \"<HTTP_METHOD>\",\n  \"url\": \"<endpoint>\",\n  \"authentication\": \"<type>\",\n  // ... method-specific fields\n}\n```\n\n**Dependencies**:\n- POST/PUT/PATCH → sendBody available\n- sendBody=true → body required\n- authentication != \"none\" → credentials required\n\n**Critical: credentials block, node id, typeVersion**\n- **Never set a placeholder credential ID** (e.g. `\"id\": \"REPLACE_ME\"`) — n8n's UI renders a permanently disabled credential selector for unknown IDs. Omit the `credentials` block when the real ID is unknown; the user then gets a normal clickable dropdown.\n- **Node `id` must be a UUID v4**, not a readable slug — the frontend binds forms and the credential component to it.\n- **Don't hardcode old `typeVersion` values** — verify the current version with `get_node` (httpRequest is 4.4+).\n\n### Pattern 3: Database Nodes\n\n**Examples**: Postgres, MySQL, MongoDB\n\n**Structure**:\n```javascript\n{\n  \"operation\": \"<query|insert|update|delete>\",\n  // ... operation-specific fields\n}\n```\n\n**Dependencies**:\n- operation=\"executeQuery\" → query required\n- operation=\"insert\" → table + values required\n- operation=\"update\" → table + values + where required\n\n**Critical: Write operations may return 0 items**\n- INSERT, UPDATE, DELETE can produce 0 n8n output items, depending on the node and operation (raw query execution reliably returns 0 result rows; some database nodes return the affected rows)\n- Set `alwaysOutputData: true` on write-operation nodes to keep downstream chains alive\n- Downstream nodes should use `$('UpstreamNode').all()` instead of `$input` if they need data\n\n### Pattern 4: Conditional Logic Nodes\n\n**Examples**: IF, Switch, Merge\n\n**Structure**:\n```javascript\n{\n  \"conditions\": {\n    \"<type>\": [\n      {\n        \"operation\": \"<operator>\",\n        \"value1\": \"...\",\n        \"value2\": \"...\"  // Only for binary operators\n      }\n    ]\n  }\n}\n```\n\n**Dependencies**:\n- Binary operators (equals, contains, etc.) → value1 + value2\n- Unary operators (isEmpty, isNotEmpty) → value1 only + singleValue: true\n\n---\n\n## Operation-Specific Configuration\n\nRequired fields shift with resource + operation: Slack `post` needs `channel`+`text`, but `update` needs `messageId`+`text` (channel optional) and `channel/create` needs `name`. HTTP `GET` uses `sendQuery`+`queryParameters`; `POST` needs `sendBody`+`body`. IF binary operators (`equals`) need `value1`+`value2`; unary (`isEmpty`) need only `value1` plus auto-added `singleValue: true`. Concrete minimal configs for each in **[OPERATION_PATTERNS.md](OPERATION_PATTERNS.md#operation-specific-configuration-examples)**.\n\n---\n\n## Handling Conditional Requirements\n\nSome fields are required only under certain conditions: HTTP `body` is required when `sendBody=true` AND `method IN (POST, PUT, PATCH, DELETE)`; IF `singleValue` should be `true` when the operator is unary (`isEmpty`, `isNotEmpty`, `true`, `false`) — and auto-sanitization sets it for you. Discover conditional requirements by reading the validation error, searching the property (`get_node({mode: \"search_properties\"})`), or iterating from a minimal config. Worked discovery examples in **[DEPENDENCIES.md](DEPENDENCIES.md#handling-conditional-requirements)**.\n\n---\n\n## Node-Specific Configuration Notes\n\n### SplitInBatches v3\n\n```javascript\n{\n  \"batchSize\": 100,  // Number of items per batch\n  \"options\": {}\n}\n```\n\n**Output wiring**:\n- `main[0]` (done) → Connect to downstream processing (add Limit 1 first)\n- `main[1]` (each batch) → Connect to loop body, then loop back to SplitInBatches input\n\nSee the n8n Workflow Patterns skill for detailed loop and nested loop patterns.\n\n### Google Sheets Node\n\n**Per-item execution**: Each input item triggers a separate API call. If you have 100 items and use a Google Sheets \"Append Row\" node, it makes 100 API calls. To write in bulk, aggregate items in a Code node first, then use a single HTTP Request with the Sheets API.\n\n**Formula columns**: Never use `append` on sheets with formula columns — it overwrites formulas. Instead, use HTTP Request with Google Sheets API `values.update` (PUT) method and a `googleApi` credential.\n\n---\n\n## Configuration Anti-Patterns\n\n### ❌ Don't: Over-configure Upfront\n\n**Bad**:\n```javascript\n// Adding every possible field\n{\n  \"method\": \"GET\",\n  \"url\": \"...\",\n  \"sendQuery\": false,\n  \"sendHeaders\": false,\n  \"sendBody\": false,\n  \"timeout\": 10000,\n  \"ignoreResponseCode\": false,\n  // ... 20 more optional fields\n}\n```\n\n**Good**:\n```javascript\n// Start minimal\n{\n  \"method\": \"GET\",\n  \"url\": \"...\",\n  \"authentication\": \"none\"\n}\n// Add fields only when needed\n```\n\n### ❌ Don't: Skip Validation\n\n**Bad**:\n```javascript\n// Configure and deploy without validating\nconst config = {...};\nn8n_update_partial_workflow({...});  // YOLO\n```\n\n**Good**:\n```javascript\n// Validate before deploying\nconst config = {...};\nconst result = validate_node({...});\nif (result.valid) {\n  n8n_update_partial_workflow({...});\n}\n```\n\n### ❌ Don't: Ignore Operation Context\n\n**Bad**:\n```javascript\n// Same config for all Slack operations\n{\n  \"resource\": \"message\",\n  \"operation\": \"post\",\n  \"channel\": \"#general\",\n  \"text\": \"...\"\n}\n\n// Then switching operation without updating config\n{\n  \"resource\": \"message\",\n  \"operation\": \"update\",  // Changed\n  \"channel\": \"#general\",  // Wrong field for update!\n  \"text\": \"...\"\n}\n```\n\n**Good**:\n```javascript\n// Check requirements when changing operation\nget_node({\n  nodeType: \"nodes-base.slack\"\n});\n// See what update operation needs (messageId, not channel)\n```\n\n---\n\n## Surgical Field Edits with patchNodeField\n\nWhen you need to edit a specific string inside a node field — rather than replacing the whole field — use `patchNodeField` in `n8n_update_partial_workflow`. This is especially useful for:\n\n- Editing code inside Code nodes without re-transmitting the full code block\n- Updating URLs or text in large HTML email templates\n- Fixing typos in JSON bodies or long text fields\n\n```javascript\n// Instead of replacing the entire jsCode field:\nn8n_update_partial_workflow({\n  id: \"wf-123\",\n  operations: [{\n    type: \"patchNodeField\",\n    nodeName: \"Code\",\n    fieldPath: \"parameters.jsCode\",\n    patches: [{find: \"const limit = 10;\", replace: \"const limit = 50;\"}]\n  }]\n})\n```\n\n`patchNodeField` is strict — it errors if the find string isn't found or matches multiple times (unless `replaceAll: true`). This prevents accidental silent failures during configuration updates. See the n8n MCP Tools Expert skill for full syntax and examples.\n\n---\n\n## Best Practices\n\n### Do\n\n1. **Start with get_node (standard detail)**\n   - ~1-2K tokens response\n   - Covers 95% of configuration needs\n   - Default detail level\n\n2. **Validate iteratively**\n   - Configure → Validate → Fix → Repeat\n   - Average 2-3 iterations is normal\n   - Read validation errors carefully\n\n3. **Use search_properties mode when stuck**\n   - If field seems missing, search for it\n   - Understand what controls field visibility\n   - `get_node({mode: \"search_properties\", propertyQuery: \"...\"})`\n\n4. **Respect operation context**\n   - Different operations = different requirements\n   - Always check get_node when changing operation\n   - Don't assume configs are transferable\n\n5. **Trust auto-sanitization**\n   - Operator structure fixed automatically\n   - Don't manually add/remove singleValue\n   - IF/Switch metadata added on save\n\n### ❌ Don't\n\n1. **Jump to detail=\"full\" immediately**\n   - Try standard detail first\n   - Only escalate if needed\n   - Full schema is 3-8K tokens\n\n2. **Configure blindly**\n   - Always validate before deploying\n   - Understand why fields are required\n   - Use search_properties for conditional fields\n\n3. **Copy configs without understanding**\n   - Different operations need different fields\n   - Validate after copying\n   - Adjust for new context\n\n4. **Manually fix auto-sanitization issues**\n   - Let auto-sanitization handle operator structure\n   - Focus on business logic\n   - Save and let system fix structure\n\n---\n\n## Silent-Failure Gotchas by Node Family\n\nSome misconfigurations pass `validate_node` and `validate_workflow` clean, run without error, and quietly do the wrong thing — `get_node` shows the fields exist but not what happens when you omit them. The high-frequency ones:\n\n- **Switch** — no `options.fallbackOutput` ⇒ unmatched items silently dropped.\n- **Merge** — `numberOfInputs` defaults to 2 (extra sources drop); `useDataOfInput` is 1-indexed vs the 0-indexed `connections.<src>.main[idx]` slot (`useDataOfInput: \"N\"` → `main[N-1]`).\n- **Database** — `{{ }}` interpolation into `parameters.query` is SQL injection; use `$1/$2` placeholders + `options.queryReplacement`.\n- **Slack** — Block Kit must be wrapped `={{ { \"blocks\": ... } }}` or it posts as plain text.\n- **Webhook / Respond** — `responseCode` defaults to 200 even on error branches.\n- **Schedule Trigger** — timezone is workflow-level (Workflow Settings), not per-rule.\n\nFull symptom/cause/fix detail (in JSON + `n8n_update_partial_workflow` terms) in **[NODE_FAMILY_GOTCHAS.md](NODE_FAMILY_GOTCHAS.md)**.\n\n---\n\n## Detailed References\n\nFor comprehensive guides on specific topics:\n\n- **[DEPENDENCIES.md](DEPENDENCIES.md)** - Deep dive into property dependencies and displayOptions\n- **[OPERATION_PATTERNS.md](OPERATION_PATTERNS.md)** - Common configuration patterns by node type\n- **[NODE_FAMILY_GOTCHAS.md](NODE_FAMILY_GOTCHAS.md)** - Silent runtime traps by family (Switch, Merge, Database, Slack, Webhook, Schedule)\n\n---\n\n## Summary\n\n**Configuration Strategy**:\n1. Start with `get_node` (standard detail is default)\n2. Configure required fields for operation\n3. Validate configuration\n4. Search properties if stuck\n5. Iterate until valid (avg 2-3 cycles)\n6. Deploy with confidence\n\n**Key Principles**:\n- **Operation-aware**: Different operations = different requirements\n- **Progressive disclosure**: Start minimal, add as needed\n- **Dependency-aware**: Understand field visibility rules\n- **Validation-driven**: Let validation guide configuration\n\n**Related Skills**:\n- **n8n MCP Tools Expert** - How to use discovery tools correctly\n- **n8n Validation Expert** - Interpret validation errors\n- **n8n Expression Syntax** - Configure expression fields\n- **n8n Workflow Patterns** - Apply patterns with proper configuration\n","data/skills/n8n-subworkflows/SKILL.md":"---\nname: n8n-subworkflows\ndescription: Build reusable, composable n8n sub-workflows. Use when extracting shared logic, building anything multi-step or reused across workflows, or any workflow over ~10 nodes — and whenever the user mentions sub-workflows, Execute Workflow, reuse, shared/common logic, modular workflows, \"Define Below\" inputs, waitForSubWorkflow, mode each vs all, or exposing a workflow as an agent tool. Covers typed sub-workflow inputs, all-vs-each execution, verb-first naming for discovery, stateless vs stateful design, and splitting by input shape.\n---\n\n# n8n Sub-workflows\n\nA sub-workflow is a reusable function. An **Execute Workflow Trigger** declares typed inputs, the body does the work, and the last node returns the output. A caller invokes it through an **Execute Workflow** node like any other step.\n\nThat framing buys you the things functions buy you everywhere: encapsulation, reuse, testability, replaceability. It's the primary reuse mechanism in n8n, and it's badly underused. Without it, the same logic gets copy-pasted across workflows — then a bug gets fixed in two places, the third copy gets missed, and your \"identical\" copies quietly drift apart.\n\nThis skill is about when to reach for a sub-workflow, how to define its input/output contract so callers (and agents) can actually use it, how to call it correctly (`all` vs `each`, blocking vs fire-and-forget), and how to name it so it gets found instead of rebuilt.\n\n---\n\n## The two non-negotiables\n\nEverything else is judgement. These two are not.\n\n### 1. Search before you build\n\nBefore you write logic for a generic problem, check whether a sub-workflow already does it. The community MCP can't filter workflows by tag, so the **name is the discovery surface**:\n\n```\nn8n_list_workflows()                          # scan the library\nn8n_get_workflow({ id: \"<candidate>\" })       # read its inputs/outputs + body\n```\n\nIf something fits, use it and tell the user (\"I found `Subworkflow: Parse RFC2822 date` — using that\"). If nothing fits, build it *with a discoverable name* so the next search finds it. The discovery convention (verb-first prefixes) lives in **NAMING_AND_DISCOVERY.md**.\n\n### 2. The Execute Workflow Trigger uses \"Define Below\" with typed fields — not passthrough\n\nThe trigger has two input modes. **Default to \"Define Below\"** with explicit typed fields. Define Below is the only mode that gives callers a schema to fill — it's what lets an AI agent pass values via `$fromAI` and what lets structured callers map fields cleanly. Passthrough has no schema, so the trigger can't be wired as a clean agent tool and structured callers have nothing to bind to.\n\nTwo exceptions, and only two:\n\n- **Binary input.** Typed fields are JSON-only. If the sub-workflow must receive an image/file/PDF, you need passthrough so the `binary` slot flows through.\n- **Zero inputs.** Define Below requires at least one field. A genuinely no-arg operation (\"list active credentials\", \"current count\") has nowhere to put an empty schema, so passthrough is the only option.\n\nOutside those two cases, passthrough is a bug. See \"Inputs and outputs as a contract\" below.\n\n---\n\n## Should this be a sub-workflow?\n\nYou're about to write a chunk of logic. Run it through this:\n\n```\nCould this plausibly be needed in another workflow?\n  └─ Yes → extract.\n\nIs it a generic concern (auth, retry, parsing, formatting, ID generation)?\n  └─ Almost always → extract. These are the canonical reusable sub-workflows.\n\nIs it >5 nodes and conceptually one thing?\n  └─ Probably extract, even if reuse isn't certain. It's better isolated.\n\nIs it one HTTP call with no logic around it?\n  └─ Don't. A sub-workflow that's just trigger → HTTP → return adds a boundary\n     for nothing.\n\nIs it tightly coupled to this one caller's data shape?\n  └─ Don't extract yet — fix the data shape first, or you just relocate the coupling.\n```\n\nThe reasons to extract go beyond reuse:\n\n- **Readability.** The caller shows one node (\"Parse date\") instead of five.\n- **Testability.** Run the sub-workflow alone with pinned input: `n8n_test_workflow({workflowId, method: \"prepare\"})` names the nodes that need data, then `method: \"pinned\"` runs it with what you build (needs `N8N_MCP_ACCESS_TOKEN` and the workflow's \"Available in MCP\" setting — see **n8n-mcp-tools-expert**). A sub-workflow has no HTTP trigger, so the default `method: \"auto\"` cannot run it.\n- **Replaceability.** Swap the implementation without rippling to callers.\n\nA 20-node workflow is fine *if it's mostly a linear sequence of Execute Workflow calls and decisions* — each node has one purpose, and you inspect a section by opening the sub-workflow it calls. A 20-node workflow of inline transformations is not fine. If yours has 15+ nodes and isn't mostly sub-workflow calls and branches, extract more.\n\n---\n\n## Stateless vs. stateful (deliberately)\n\nBoth are first-class. The choice is about intent and what the contract promises.\n\n**Stateless** — input in, output out, no I/O beyond that. The default for pure logic. When you need it again, you call it without worrying about side effects firing.\n\n- `Subworkflow: Parse RFC2822 date` — date string → ISO date or error.\n- `Subworkflow: Compute MRR from subscription` — subscription object → number.\n- `Subworkflow: Format invoice as HTML` — invoice data → HTML string.\n\n**Stateful (deliberate)** — reads or writes external state *behind a clean contract*. This is the repository pattern: the sub-workflow abstracts the storage operation so callers think in domain terms, not SQL.\n\n- `Customer: get by id` — id → customer object or `{ ok: false, error: \"not_found\" }`. Reads the DB.\n- `Customer: write billing record` — record → `{ ok: true, id }`. Writes the DB.\n- `Notify: send to on-call` — channel, message → `{ ok: true, messageId }`. Calls Slack/SMTP.\n\nWhy build these as sub-workflows: callers think `get customer by id` instead of writing the query; you can swap the store (Postgres → Supabase, native node → HTTP) without touching a single caller; and idempotency, retry, and validation get centralized in one place.\n\nWhat to avoid is **accidental state** — a sub-workflow named and described as pure that quietly writes to a log table. That ambushes every caller who reasonably assumed it was safe to retry or compose. Either make the side effect part of the contract (rename it, document it, return its result) or move it out.\n\n---\n\n## Inputs and outputs as a contract\n\nThe trigger's declared fields and the last node's output shape *are* the sub-workflow's API. Treat them like one.\n\n### Declaring typed inputs (Define Below)\n\nEach declared input is a typed parameter the caller fills. Pick types deliberately (`string`, `number`, `boolean`, `array`, `object`) — an agent uses these as the required types when filling tool parameters, and humans rely on them when wiring callers. The trigger node parameters look like this:\n\n```json\n{\n  \"type\": \"n8n-nodes-base.executeWorkflowTrigger\",\n  \"parameters\": {\n    \"workflowInputs\": {\n      \"values\": [\n        { \"name\": \"list_of_ids\",        \"type\": \"array\" },\n        { \"name\": \"include_transcript\", \"type\": \"boolean\" },\n        { \"name\": \"session_id\",          \"type\": \"string\" }\n      ]\n    }\n  }\n}\n```\n\nInside the body, read them as `$json.list_of_ids`, or from anywhere downstream as `$('When Executed by Another Workflow').first().json.<field>` (see **n8n-expression-syntax**).\n\n### The contract rules\n\n- **Document inputs and outputs in the workflow `description`.** Field names, types, purpose, and a few representative keywords. The description is what callers (human and agent) read for the contract, and it's what `n8n_list_workflows` matches against.\n- **Return consistent, natural shapes — not storage shapes.** A sub-workflow that owns a Data Table or an S3 file hides that representation from callers. Arrays return as arrays, objects as objects, dates as ISO strings — regardless of whether the underlying storage was JSON-stringified text. The return contract is the *interface*; the storage layout is *implementation detail*. Common slip: a sub-workflow with a \"fresh\" path (just-computed, natural shape) and a \"cached\" path (just read from a stringified column). Wrong instinct: stringify the fresh path to match the cached one. Right instinct: parse the cached path so both return the natural shape.\n- **Return errors, don't always throw.** For *expected* failures (a parse error, a not-found), return `{ ok: false, error: \"...\" }` so the caller can branch without wiring an error output. Reserve throwing for genuinely unexpected failures — see **n8n-error-handling**.\n- **The contract is frozen once it has callers.** Adding *optional* fields is safe. Renaming or removing a field is dangerous: n8n won't error on an unrecognized input field — the body just sees `undefined`, the caller has no idea, and you get a silent contract break. To change a field, enumerate every caller (`n8n_list_workflows` + inspect each one's Execute Workflow node), migrate them in the same change, and verify with `validate_workflow` and `n8n_get_workflow` before you're done.\n\n### The final Return node — the legitimate Set exception\n\nShape the output with a final **Set / Edit Fields** node, named `Return` or `Return <thing>`. This is the one place a Set node earns its keep against the usual \"don't add a trailing Set node\" advice from **n8n-expression-syntax**: the implicit consumer of a sub-workflow's last node is *every caller*, so an explicit Set makes the return contract visible — a reader sees the whole API by reading one node, and you strip any noise fields the last computation node carried.\n\n---\n\n## Calling sub-workflows: `mode` and `waitForSubWorkflow`\n\nTwo settings on the caller's **Execute Workflow** node decide how the sub-workflow runs.\n\n### `mode`: `all` vs `each`\n\n| `mode` | Sub-workflow runs | Items per run |\n|---|---|---|\n| `all` (default) | once | all N items (flowing per-item through nodes as usual) |\n| `each` | N times | exactly one item per run |\n\nFor a body that just processes items the normal way, the two are equivalent — n8n nodes iterate per-item either way. **The split only matters when the body assumes it sees exactly one item**: a per-run aggregation, \"this is THE customer to act on\" logic, or a final write that should fire once per input. With `all`, that body gets all N items at once and the assumption breaks (you aggregate everyone into one result instead of one-per-input). With `each`, each invocation gets one item and the assumption holds.\n\nSo: when you need per-item iteration, prefer `mode: each` over dropping a Loop Over Items node *inside* the sub-workflow. The mode does the iteration for you, and the body stays simple and single-item.\n\n### `waitForSubWorkflow`: `true` vs `false`\n\n`waitForSubWorkflow` defaults to `true` — the caller blocks until the sub-workflow returns, then continues with its output. Set `options.waitForSubWorkflow: false` to fire-and-forget: the call dispatches, the caller moves on immediately, the sub-workflow runs in the background, and downstream sees no return data.\n\n### The only true parallelization n8n offers\n\n`mode: each` + `waitForSubWorkflow: false` is **the only way to get genuinely concurrent sub-workflow execution**: N items dispatch N runs that execute in parallel (still bounded by per-instance concurrency limits). The caller doesn't know when — or whether — any of them finished, so it's only useful with a separate completion-tracking mechanism, typically a Data Table the sub-workflow updates as it progresses. The full stage → dispatch → poll pattern is in **SUBWORKFLOW_PATTERNS.md** (\"Fire-and-forget parallelization\").\n\n---\n\n## Splitting by input shape (the N+1 pattern)\n\nWhen a sub-workflow has multiple input paths whose contracts *genuinely* differ — binary vs JSON, sync vs async, divergent auth schemes — don't cram them under one trigger with passthrough + an internal Switch. The forcing function is real: passthrough (for binary or zero-input) and Define Below (for typed inputs) are mutually exclusive on a single trigger. The reflex to \"pick passthrough because it's most permissive, then branch inside\" costs you the typed schema (no clean agent tool), grows branch-shape cruft, and turns every new input shape into more branching.\n\nThe fix: for N divergent input contracts, build **N+1 sub-workflows** — one outer per contract, each doing its input-specific prep (validation, fetching, hashing, extraction) and calling **one shared downstream** sub-workflow with a normalized shape. The shared core has a single typed input contract and knows nothing about which outer called it. The worked example (process a paper from an external ID *or* an uploaded PDF) is in **SUBWORKFLOW_PATTERNS.md**.\n\n---\n\n## Sub-workflow as an agent tool\n\nA sub-workflow with a typed Define Below trigger doubles as an AI-agent tool: the agent fills the declared fields via `$fromAI`, the body runs, the result comes back as the tool observation. This is the high-value reason to default to Define Below — passthrough triggers can't expose a fill-able schema.\n\nThe zero-input case still works as a tool: the agent's only decision is whether to invoke. The binary case does *not* wire cleanly as a tool, because agents can't pass binary directly.\n\nFor tool naming, descriptions, and the binary-input workaround, see **n8n-agents**; for the binary handling itself, **n8n-binary-and-data**.\n\n---\n\n## Anti-patterns\n\n| Anti-pattern | What goes wrong | Fix |\n|---|---|---|\n| Duplicating the same logic in three workflows | A bug gets fixed in two places, the third drifts | Extract once to a named sub-workflow |\n| Building a new sub-workflow without searching | The library grows duplicates; future searches find both | `n8n_list_workflows` / `n8n_get_workflow` first |\n| Trigger set to passthrough when not handling binary and not zero-input | No schema → agents can't fill params, structured callers can't bind | Use Define Below with typed `workflowInputs.values` |\n| Zero-input passthrough with no clear-and-document | Body silently reads stray fields from whatever the caller forwarded | Start with a Set (\"Keep Only Set\", no fields) and a sticky noting \"no inputs expected\" |\n| Sub-workflow named/described as pure that quietly writes state | Callers can't reason about retry/idempotency; the side effect ambushes them | Make the side effect part of the contract, or move it out |\n| Sub-workflow with no `description` | Won't be found in future searches; nobody knows what it does | Set `description` with input/output shape + keywords |\n| Name like `Helper 3` / no prefix | Doesn't say what it does, matches no prefix search | Verb-first prefix (`Subworkflow:`, `<Domain>:`, `Tool:`) |\n| `mode: all` on a body that assumes one item | Aggregates all inputs into one result instead of one-per-input | `mode: each` (and skip the internal Loop Over Items) |\n| Renaming a live input field without migrating callers | Callers send the old name → body sees `undefined`, no error anywhere | Migrate every caller in the same change; verify with `validate_workflow` |\n| 30-node workflow with no extraction | Hard to read, test, and replace | Extract logical sections into sub-workflows |\n\n---\n\n## What's NOT available via the community MCP\n\n| Want to do | Reality |\n|---|---|\n| Filter/discover workflows by **tag** | The MCP can't read or filter by tags (UI-only). Discovery is the *name* — use verb-first prefixes and `n8n_list_workflows`. |\n| Catch an **unrecognized input field** | n8n doesn't error on one. The body sees `undefined` and the caller never knows — a silent contract break. Verify field renames by hand across callers. |\n| Set the input mode / fields without a typed trigger | The trigger node itself must declare `workflowInputs.values`. Configure it with `n8n_update_partial_workflow` (`updateNode` / `patchNodeField`); validate with `get_node` / `validate_node`. |\n\nWhat the MCP **can** do: build the sub-workflow and its callers (`n8n_update_partial_workflow` with `addNode` / `addConnection` / `updateNode` / `patchNodeField`), discover existing ones (`n8n_list_workflows`, `n8n_get_workflow`), validate (`validate_workflow`, `n8n_validate_workflow`), test in isolation (`n8n_test_workflow`), inspect runs (`n8n_executions`), back a stateful sub-workflow with a Data Table (`n8n_manage_datatable`), and activate (`activateWorkflow`).\n\n---\n\n## Reference files\n\n| File | Read when |\n|---|---|\n| **SUBWORKFLOW_PATTERNS.md** | `mode: all` vs `each` in depth, splitting by input shape (the N+1 worked example), fire-and-forget parallelization with Data Table polling |\n| **NAMING_AND_DISCOVERY.md** | Naming a new sub-workflow, the verb-first prefix convention, searching for existing ones, writing a discoverable description |\n\n---\n\n## Integration with other skills\n\n- **n8n-workflow-patterns** — use it for the overall shape of the orchestrating workflow; use this skill to decide which sections become sub-workflows.\n- **n8n-mcp-tools-expert** — parameter formats for `n8n_list_workflows`, `n8n_get_workflow`, `n8n_update_partial_workflow`, and `n8n_manage_datatable` (the Data Table behind a stateful sub-workflow and the fire-and-forget poll).\n- **n8n-node-configuration** — `workflowInputs` and the `inputSource` (Define Below vs passthrough) toggle are displayOptions-driven config on the Execute Workflow Trigger.\n- **n8n-expression-syntax** — reading inputs (`$json`, `$('When Executed by Another Workflow')`) and the legitimate final-Set exception both live here.\n- **n8n-error-handling** — expected failures return `{ ok: false, error }`; unexpected ones throw and route through error outputs. A sub-workflow boundary is a natural place to define that line.\n- **n8n-validation-expert** — validate the sub-workflow and its callers; an unrecognized input field won't surface here, so verify field changes manually.\n- **n8n-code-javascript / n8n-code-python** — when a sub-workflow's body is a single Code node, its contract is still the trigger's typed inputs and the returned shape, not the Code node's internals.\n- **n8n-code-tool** — the Custom Code Tool is the *inline* agent-tool option; a sub-workflow tool is the reusable, multi-step one. Pick the sub-workflow when the logic is shared across agents or needs the full Code-node sandbox.\n- **n8n-agents** — wiring a typed sub-workflow as an agent tool, including the zero-input and binary cases.\n- **n8n-binary-and-data** — passthrough triggers for binary input, and why binary can't flow through an agent tool directly.\n- **using-n8n-mcp-skills** — when to consult which skill across a build.\n\n---\n\n## Quick reference checklist\n\nBefore shipping a sub-workflow:\n\n- [ ] **Searched first** with `n8n_list_workflows` / `n8n_get_workflow` — it doesn't already exist\n- [ ] **Trigger uses Define Below** with typed `workflowInputs.values` (unless binary or zero-input)\n- [ ] **Zero-input passthrough** (if used) starts with a \"Keep Only Set\" Set node + a sticky noting no inputs\n- [ ] **Name** has a verb-first prefix (`Subworkflow:`, `<Domain>:`, `Tool:`)\n- [ ] **Description** documents input/output shape and carries searchable keywords\n- [ ] **Returns a natural, consistent shape** via a final `Return` Set node — not a storage shape\n- [ ] **Expected failures** return `{ ok: false, error }`; only unexpected ones throw\n- [ ] **Caller `mode`** is `each` if the body assumes a single item (not an internal Loop Over Items)\n- [ ] **`waitForSubWorkflow`** is set deliberately (`false` only with a completion-tracking mechanism)\n- [ ] **Stateful sub-workflows** declare their side effect in name + description — no accidental state\n- [ ] **Validated** with `validate_workflow`; tested in isolation with `n8n_test_workflow`\n\n---\n\n**Remember**: a sub-workflow is a function. Its API is the trigger's typed inputs and the last node's output shape — make both explicit, name it so it's found, and call it with the `mode` its body expects. A passthrough trigger that isn't for binary or a zero-arg op, or a name nobody can search, is how a reusable function quietly becomes the next duplicate.\n","data/skills/n8n-error-handling/SKILL.md":"---\nname: n8n-error-handling\ndescription: Wire n8n error handling so failures are loud, structured, and recoverable. Use when building any webhook/API workflow, a scheduled or unattended workflow, or any path where a silent failure would drop user-visible work — and whenever the user mentions error handling, onError, continueErrorOutput, error branches/outputs, retries, retryOnFail, Respond to Webhook status codes, 4xx/5xx, Error Trigger, or \"my workflow fails silently\". Covers per-node error outputs and wiring, retry/self-healing, error-trigger workflows, and 4xx/5xx response shapes.\n---\n\n# n8n Error Handling\n\nBy default, when an n8n node throws, the **whole workflow halts**. For an interactive run you're watching, that's fine — you see the red node and fix it. For anything unattended (a webhook API, a cron job, a queue worker, an agent tool), it's the wrong default: the caller gets a timeout or an empty 500, the operator gets no alert, and the symptom is \"the integration just stopped working\" with no log and no clue.\n\nThis skill is about making failures **loud, structured, and recoverable** — and, best case, **self-healing** so transient blips never reach a human at all.\n\nThe two ideas that prevent most silent failures:\n\n- **Per-node error outputs** — a node's failure routes down a second output you control, instead of killing the run.\n- **A workflow-level error workflow** — a catch-all that fires for anything that escapes per-node handling (timeouts, crashes between nodes, unwired failures).\n\n---\n\n## When you actually need this\n\n| Workflow shape | Error handling posture |\n|---|---|\n| Webhook / API (anything with `Respond to Webhook`) | **Required.** Every fallible node's error output wired; status code matches cause. |\n| Scheduled / cron / queue worker / agent tool (unattended) | **Required.** A workflow-level error workflow, plus `retryOnFail` on network nodes. |\n| Internal one-off you run and watch yourself | **Optional.** Default `onError: \"stopWorkflow\"` is fine — you'll see the red node and re-run. |\n\nThe dividing line: **if anyone other than you sees the output** — a downstream system, an end user, an on-call engineer — the failure has to be handled, not swallowed. If you're the only watcher and the cost of failure is \"I notice and re-run\", looser is fine.\n\n---\n\n## The #1 silent trap: per-node error output is a TWO-step setup\n\nThis is the single most common way an n8n workflow \"handles\" errors while actually swallowing them. Routing a node's failure to a handler takes **two** changes, and doing only one looks complete but misbehaves:\n\n1. **Set `onError: \"continueErrorOutput\"`** on the node. This is what *creates* the second output. Without it, `main[1]` doesn't exist no matter what you wire.\n2. **Wire that error output** (`connections.<node>.main[1]`, i.e. `sourceIndex: 1`) to a real handler. Without a target, the error data is emitted into the void.\n\nGet one without the other and you hit a failure mode:\n\n| What you did | What happens at runtime |\n|---|---|\n| `onError` set, error output **not** wired | Error data is silently discarded. Downstream doesn't fire. The dashboard shows the run as **succeeded**. Worst case — no error logged anywhere. |\n| Error output wired, `onError` **not** set | The slot never fires; the handler is unreachable. On failure the workflow just **halts** (default `stopWorkflow`). |\n| Both done | Failure routes down `main[1]` to your handler. ✅ |\n\n### Doing both with `n8n_update_partial_workflow`\n\n```javascript\n// 1) Turn on the error output (creates main[1])\n{ type: \"updateNode\", nodeName: \"HTTP Request\",\n  changes: { onError: \"continueErrorOutput\" } }\n\n// 2) Wire the error output to a handler. sourceIndex: 1 = the error output.\n{ type: \"addConnection\",\n  source: \"HTTP Request\",\n  target: \"Handle Error\",\n  sourceIndex: 1 }\n```\n\n`sourceIndex: 0` is the success path, `sourceIndex: 1` is the error path. (For IF nodes the aliases `branch: \"true\"`/`\"false\"` map to index 0/1; for a generic fallible node, use the explicit `sourceIndex: 1`.)\n\n**Then verify.** This trap doesn't surface in `validate_workflow` — a half-wired error output validates clean. Pull the workflow with `n8n_get_workflow` and confirm **both** halves:\n\n- The node's `onError` is `\"continueErrorOutput\"`.\n- `connections[\"HTTP Request\"].main[1]` contains your handler.\n\nValid `onError` values:\n\n| Value | Effect |\n|---|---|\n| `\"stopWorkflow\"` (default) | Error halts the whole workflow. |\n| `\"continueRegularOutput\"` | Error item flows out the **normal** output. Rare, usually wrong — downstream gets error-shaped data and keeps going. |\n| `\"continueErrorOutput\"` | Error item flows out the **separate** error output (`main[1]`). The one you wire. |\n\nFull failure-mode catalog, fan-in/fan-out shapes, and verification: **NODE_ERROR_OUTPUTS.md**.\n\n---\n\n## Self-healing first: `retryOnFail` before you wire error paths\n\nBefore you build error branches, absorb the transient failures so they never reach those branches. On **any node that calls a network service** — HTTP Request, comms (Gmail/Slack/Discord), databases, AI nodes, third-party integrations — set node-level retry:\n\n```javascript\n{ type: \"updateNode\", nodeName: \"HTTP Request\",\n  changes: {\n    retryOnFail: true,\n    maxTries: 3,\n    waitBetweenTries: 5000   // ms\n  } }\n```\n\nWhy this comes **first**: a 429 or a brief upstream hiccup will retry and usually succeed on its own. The error output then fires only on *real, persistent* failures — so your 5xx responses and on-call alerts reflect actual problems instead of noise.\n\nEngine limits to know: retry fires on **any** error (there's no per-status-code filter), `maxTries` caps at 5, and `waitBetweenTries` caps at 5000ms — so 5000 is both the max and a sensible default. See **n8n-node-configuration** (NODE_FAMILY_GOTCHAS.md) for node-specific notes.\n\n---\n\n## API workflows: the canonical shape\n\nA webhook-triggered workflow that responds to its caller has one rule that overrides everything else: **no hanging branches**. Every path — success and every error — must end at a `Respond to Webhook`, or the caller sits there until it times out.\n\n```\nWebhook (responseMode: \"responseNode\")\n  ├── validate input → process → Respond (200, body)\n  └── (any fallible node's error output → sourceIndex 1)\n            → Respond (4xx/5xx, structured error body)\n            → optional: log full error privately / notify\n```\n\nThree things make this work:\n\n1. **Fan-in to one error responder.** Many fallible nodes can route their `main[1]` to a single `Respond` node. Keeps the graph readable.\n2. **Validation failures (4xx) are checked *upstream*, not via error outputs.** A missing field isn't a node *crashing* — it's an expected outcome with a known response. Branch on it with IF/Switch (or the schema validator below) and return 400/401/403/404 directly. Error outputs are for *unexpected* failures (5xx).\n3. **`responseCode` defaults to 200 — even on error branches.** This is its own silent trap (see RESPONSE_SHAPES.md and **n8n-node-configuration** NODE_FAMILY_GOTCHAS.md): an error branch that returns 200 with an error body looks like success to the caller's HTTP client, so their error handling never fires. Set `responseCode` explicitly on every Respond node.\n\n### Input validation: the Set-node schema validator\n\nFor any endpoint doing structured input validation, run the check as an IIFE inside a single **Set** node rather than a chain of IF/Switch nodes per field. One node validates the whole payload, returns `{ valid, validationError, details, requiredSchema }`, and an IF branches on `valid` → your logic (200) or a 400 Respond that echoes the schema back so the caller can self-correct. It's also dramatically faster than a recursive validator in a Code node + sub-workflow. The full pattern, the constraint cookbook, and the expression-escaping gotchas live in **API_WORKFLOWS.md**.\n\n---\n\n## Response shapes: map cause → status code\n\nA 5xx with `text/plain \"Internal Server Error\"` is technically an error response and practically useless. And not every failure is a 5xx. **Match the status code to *why* the request failed**, because the caller branches on it: their monitoring alerts on 5xx (your fault) but not 4xx (their fault), and 5xx suggests \"retry\" while 4xx suggests \"don't\".\n\n**The common mistake:** wiring everything — including bad input — to one `Respond` that returns 500 `internal_error`. Now the caller can't tell their bug from your outage, and your error rates can't separate real incidents from client noise.\n\n| Cause | Status | `error` code | Where it's handled |\n|---|---|---|---|\n| Required field missing / wrong type | 400 | `validation_error` | Upstream check (schema validator / IF), not error output |\n| Auth missing or invalid | 401 | `unauthorized` | Upstream check |\n| Authenticated but not allowed | 403 | `forbidden` | Upstream check |\n| Resource ID valid in request, absent in your data | 404 | `not_found` | Branch on the lookup *result*, not its error |\n| Conflicts with current state (duplicate, race) | 409 | `conflict` | Detect with logic |\n| Caller exceeded rate limit | 429 | `rate_limit_exceeded` | Set `Retry-After` header |\n| Node threw, cause unknown | 500 | `internal_error` | Error output path |\n| Third-party API returned an error | 502 | `upstream_error` | Error output of the HTTP node |\n| Can't process right now (downstream down) | 503 | `service_unavailable` | Detect specific error, hint retry |\n| Third-party API timed out | 504 | `upstream_timeout` | Error output filtered by message |\n\nSo there are two distinct flows: **4xx is decided before the work** (IF/Switch + dedicated Respond), **5xx comes out of error outputs** (\"we tried, it broke\").\n\n**One Respond, expression-driven code.** When error paths differ only by *number and message* (same body shape, same headers), don't fan out to N Respond nodes through a Switch. The Respond node accepts expressions in both `Response Code` and body — compute the code inline:\n\n```javascript\n// Response Code field on a single Respond to Webhook:\n{{ (() => {\n    const msg = $json.error?.message || $json.message || '';\n    if (msg.includes('INVALID_ID')) return 400;\n    if (/429|too many/i.test(msg)) return 429;\n    if (/timeout/i.test(msg))      return 504;\n    if (/upstream|llm|api/i.test(msg)) return 502;\n    return 500;\n})() }}\n```\n\nReserve Switch + multiple Responds for paths that diverge *structurally* (different headers, different body shapes, redirects). Same shape with a different number is one expression-driven Respond.\n\nThe default envelope is `{ \"error\": \"<code>\", \"message\": \"<human text>\" }` — the HTTP status already says success-vs-failure, so no `ok: false` flag. **Never leak internals** (stack traces, SQL, upstream bodies, tokens) into the response — log those privately, return a sanitized message. Correlation IDs, `retry_after`, validation `details`, and the full do-not-leak list are in **RESPONSE_SHAPES.md**.\n\n---\n\n## Workflow-level error workflow (the catch-all)\n\nPer-node outputs handle the failures you anticipated on the nodes you remembered to wire. An **error workflow** catches everything else: a node you forgot to wire, a crash between nodes, a whole-workflow timeout, a trigger failure. For unattended workflows this is the safety net that turns \"it silently stopped\" into \"an alert arrived\".\n\nBuild it as a separate workflow starting with an **Error Trigger** node. n8n invokes it with the failure context:\n\n```json\n{\n  \"execution\": { \"id\": \"...\", \"url\": \"...\", \"lastNodeExecuted\": \"Fetch order\",\n    \"error\": { \"name\": \"NodeApiError\", \"message\": \"...\", \"timestamp\": 1715000000000 } },\n  \"workflow\": { \"id\": \"...\", \"name\": \"Sync Stripe customers\" }\n}\n```\n\nMinimal version — **capture → notify**:\n\n```\nError Trigger → Set (build alert from execution + error) → Slack/email (post to #incidents)\n```\n\nA good alert includes the workflow name, a link to the editor and a link to the failed execution, the failed node name, and the **real** error message (not \"Workflow failed\"). Field expressions and the optional \"fetch the failing input via the n8n node\" upgrade are in **ERROR_WORKFLOWS.md**.\n\nTwo traps worth flagging up front:\n\n- **The recursion trap.** If the error workflow notifies Slack and Slack is what's down, the error workflow fails too — and the original error vanishes. Notify on a *different* channel than your monitored workflows use (most workflows alert Slack → error workflow uses email), and add a fallback (write to a Data Table) so a failed notification still leaves a trace.\n- **A \"handled\" error won't bubble up.** If a node's error output is wired to a no-op that drops the data, n8n considers the error *handled* and the error workflow does **not** fire. Only catch per-node when you're actually doing something with the error.\n\n> **What the community MCP can't do:** assigning the error workflow (instance default or per-workflow override) is an n8n **UI setting** — Workflow Settings → Error Workflow. There is no MCP tool to set it. Build the error workflow with the MCP, then tell the user the exact UI step to wire it up, and to repeat it (or set the instance default) for every unattended workflow.\n\n---\n\n## What's NOT available via the community MCP\n\n| Want to do | Reality |\n|---|---|\n| Set a workflow's **Error Workflow** setting | UI only (Workflow Settings → Error Workflow). No MCP tool. Build the workflow, then hand the user the UI step. |\n| Toggle other **workflow settings** (Save Execution Data, timezone, timeout, caller policy) | UI only. `n8n_update_partial_workflow` has `updateSettings`, but the error-workflow assignment is not reliably exposed — confirm in the UI. |\n| Enable instance-wide error logging (Sentry, server logs) | Instance config, outside n8n workflows entirely. |\n\nWhat the MCP **can** do: build the error workflow, set `onError`/`retryOnFail` on nodes (`updateNode`/`patchNodeField`), wire error outputs (`addConnection` with `sourceIndex: 1`), validate (`validate_workflow`, `n8n_validate_workflow`), auto-fix common issues (`n8n_autofix_workflow`), test (`n8n_test_workflow`), and inspect failures (`n8n_executions`).\n\n---\n\n## Anti-patterns\n\n| Anti-pattern | What goes wrong | Fix |\n|---|---|---|\n| `onError` set but error output unwired | Error silently discarded; run shows as **succeeded** | Wire `sourceIndex: 1` to a real handler, or revert `onError` to `stopWorkflow` so it's loud |\n| Error output wired but `onError` not set | Slot never fires; handler unreachable; workflow halts on failure | Set `onError: \"continueErrorOutput\"` |\n| Webhook → process → respond, no error branch | Caller gets a timeout or n8n's generic 500 | Wire every fallible node's error output to a Respond |\n| Error branch returns 200 with an `{error}` body | Caller's client reads success; their error handling never fires | Set `responseCode` to 4xx/5xx explicitly on error Responds |\n| One 500 `internal_error` for everything | Caller can't tell their bad input from your outage | Map cause → status (4xx caller, 5xx you) |\n| Catching errors in a Code node and returning them as data | Downstream processes error-shaped data and continues | Let it throw; use `onError: \"continueErrorOutput\"` + wired path |\n| Network node with no `retryOnFail` | Every transient 429/blip surfaces as a 5xx; alerts fire on noise | `retryOnFail: true, maxTries: 3, waitBetweenTries: 5000` |\n| Switch → N Responds differing only by status code | 5 nodes for what's one Respond | Compute the code inline in one expression-driven Respond |\n| Unattended workflow with no error workflow | A genuine failure goes nowhere | Build an Error Trigger workflow + assign it in the UI |\n| Error workflow notifies the same channel the workflows monitor | Channel down → error workflow also fails → error vanishes | Use a different channel + a Data Table fallback |\n| Leaking `$json.error` (stack/SQL/tokens) into the response | Exposes internals to callers/attackers | Log privately, return a sanitized message |\n\n---\n\n## Reference files\n\n| File | Read when |\n|---|---|\n| **NODE_ERROR_OUTPUTS.md** | Wiring a per-node error output on individual fallible nodes |\n| **API_WORKFLOWS.md** | Building/reviewing a webhook → Respond workflow, including the schema validator |\n| **RESPONSE_SHAPES.md** | Defining response body conventions, status codes, and what not to leak |\n| **ERROR_WORKFLOWS.md** | Setting up the workflow-level catch-all for unattended workflows |\n\n---\n\n## Integration with other skills\n\n- **n8n-workflow-patterns** — the webhook/API and scheduled patterns are where error handling lives. Use it for the overall shape; use this skill to harden it.\n- **n8n-node-configuration** — `onError`/`retryOnFail` are node config; NODE_FAMILY_GOTCHAS.md covers the Webhook/Respond response-code traps in depth.\n- **n8n-validation-expert** — the half-wired error output (one of the two steps missing) is a connection/config audit item, not a validation error. This skill is the fix.\n- **n8n-expression-syntax** — the expression-driven `Response Code` and the alert-message expressions rely on correct `{{ }}` syntax and `$json.error` access.\n- **n8n-code-javascript / n8n-code-python** — if you catch errors *inside* a Code node, decide deliberately: re-throw to use the error output, or handle and continue. Don't return error-shaped data and pretend it succeeded.\n- **n8n-code-tool** — an agent's Code Tool surfaces thrown errors back to the LLM, which then retries; that's a different error contract from workflow nodes.\n- **n8n-binary-and-data** — file/binary operations are fallible too; wire their error outputs like any network node.\n\n---\n\n## Quick reference checklist\n\nFor an **API / webhook** workflow:\n\n- [ ] Webhook trigger uses `responseMode: \"responseNode\"`\n- [ ] Input validated upstream → 4xx Respond (schema validator or IF)\n- [ ] Every fallible node has `onError: \"continueErrorOutput\"` **and** `main[1]` wired\n- [ ] Network nodes have `retryOnFail: true, maxTries: 3, waitBetweenTries: 5000`\n- [ ] Error path ends at a Respond with an **explicit** 4xx/5xx `responseCode`\n- [ ] Status code matches cause (4xx caller, 5xx you)\n- [ ] Error body is `{ error, message }` — no stack traces, SQL, or tokens\n- [ ] Verified with `n8n_get_workflow`: both `onError` and `main[1]` present on each fallible node\n\nFor an **unattended** (scheduled/cron/queue) workflow:\n\n- [ ] Network nodes have `retryOnFail` configured\n- [ ] An Error Trigger workflow exists (capture → notify, optional retry)\n- [ ] The error workflow notifies on a different channel + has a fallback (recursion trap)\n- [ ] The error-workflow setting is assigned in the n8n UI (MCP can't do it — remind the user)\n\n---\n\n**Remember**: the default is silence. Error handling is two moves — make the failure *route* (per-node `onError` + wired output, or a catch-all error workflow) and make it *speak* (a status code and body that tell the truth). Half a move is worse than none, because it looks done.\n","data/skills/n8n-self-hosting/SKILL.md":"---\nname: n8n-self-hosting\ndescription: Deploy a production self-hosted n8n end-to-end to a fresh Linux VM over SSH, using Docker Compose behind a Caddy reverse proxy with automatic HTTPS. Use whenever the user wants to self-host, install, set up, provision, or deploy n8n on their own server/VPS/box (Hetzner, DigitalOcean, AWS EC2, bare metal, etc.) — in either single/regular mode or queue mode with workers — or to update, back up, restore, or harden such an instance. This is for SELF-HOSTED n8n (Docker), not n8n Cloud and not building workflows. The skill makes the agent ask single-vs-queue first, collect the domain/SSH/timezone inputs, generate fresh secrets on the box, and bring the stack up with TLS. Trigger on \"deploy n8n\", \"self-host n8n\", \"install n8n on my server\", \"n8n docker compose\", \"n8n queue mode / workers / scaling\", \"n8n reverse proxy / SSL\", \"back up / update my n8n\", or \"we don't want to give every user the OAuth client secret\" / \"enable the Sign in with Google button\" (credential overwrites).\n---\n\n# Deploying self-hosted n8n\n\nThis skill takes a **fresh Linux VM** (Ubuntu/Debian, root or sudo SSH) to a **running,\nHTTPS, production n8n** via Docker Compose behind **Caddy** (automatic Let's Encrypt TLS).\nIt is for **self-hosted n8n on Docker** — not n8n Cloud, and not for building workflows\n(that's the rest of this pack).\n\nTwo deployment modes. The architectures differ, so **pick the mode before doing anything**.\n\nYou drive this end-to-end over SSH: preflight → install Docker → lay down the project →\ngenerate secrets → launch → verify TLS → hand off. The template files live in `assets/`;\nthe per-mode and security depth live in the reference files named below.\n\n## Rule 0 — choose the mode (ask the user)\n\nDo not guess. Ask, then commit to one:\n\n| | **Single / regular** | **Queue** |\n|---|---|---|\n| Processes | one n8n | main + N workers |\n| Extra services | none (SQLite) | Redis (queue) + Postgres (DB) |\n| Executes workflows | in the main process | on workers, in parallel |\n| Good for | 1 user, light/moderate load, simplest ops | high volume, heavy/long executions, horizontal scale |\n| Compose | `assets/docker-compose.single.yml` | `assets/docker-compose.queue.yml` |\n| Deep dive | **`SINGLE_MODE.md`** | **`QUEUE_MODE.md`** |\n\nIf unsure, start **single** — it's the simplest correct thing and covers most needs. Moving\nto queue later means swapping the compose file and migrating SQLite→Postgres, so if the user\nalready expects real volume, start **queue**.\n\n## Rule 1 — secret hygiene (non-negotiable)\n\nA misstep here leaks client credentials. Be diligent:\n\n1. **Generate every secret fresh, on the target box.** Never copy an encryption key, DB\n   password, or `.env` from another n8n instance into this one. See `SECURITY.md` for the\n   `openssl` commands.\n2. **Secrets live only in `.env`** (mode 600), referenced by the compose as `${VAR}`. Never\n   inline a secret into `docker-compose.yml`, the Caddyfile, or anything you commit.\n3. **The `N8N_ENCRYPTION_KEY` is sacred.** It encrypts every stored credential. If it's lost\n   or changes, all saved credentials become undecryptable. Set it explicitly, and tell the\n   user to back it up **off the box**. Don't echo it into long-lived logs or chat history\n   beyond what's needed to hand it over.\n4. **Never expose internal services.** Only Caddy (80/443) is public. n8n (5678), Postgres\n   (5432), Redis (6379) stay on the private Docker network — the templates already omit their\n   host port mappings. Don't add them.\n5. **`.env` and Caddy's `caddy_data` volume (the issued certs + ACME account key) are not\n   artifacts to share.** If you're working inside a git repo, confirm `.env` is git-ignored\n   before any commit.\n\n## Inputs to collect up front\n\n- **SSH target** — `user@host` and how you authenticate (key path or the user confirms the agent already has access). Root or a sudo user.\n- **Domain** — the full hostname n8n will live at, e.g. `n8n.example.com` (→ `SUBDOMAIN=n8n`, `DOMAIN_NAME=example.com`). The user must control its DNS.\n- **TLS email** — for Let's Encrypt (`SSL_EMAIL`).\n- **Timezone** — IANA name for Schedule/Cron nodes (e.g. `Europe/Warsaw`), else `Etc/UTC`.\n- **Mode** — single or queue (Rule 0). Queue → confirm the box has enough RAM (rough floor ~4 GB; each worker wants ~1–2 GB).\n- **Optional modules** — some features (currently **Agents**) are backend modules that stay off\n  unless listed in `N8N_ENABLED_MODULES`. Ask only if the user brings one up; if they do, read\n  the modules section of `QUEUE_MODE.md` before enabling it, because in queue mode it has to\n  reach the workers as well.\n\n## The deploy flow\n\nWork through these in order. `SINGLE_MODE.md` / `QUEUE_MODE.md` give the mode-specific command\ndetail; `SECURITY.md` covers secret generation and hardening; `DAY2.md` covers update/backup/restore.\n\n### 1. Preflight (the cheapest failure is the one you catch here)\n- SSH in; confirm the OS is Debian/Ubuntu-like (`. /etc/os-release`).\n- **DNS must already point at the box.** Compare the box's public IP (`curl -s ifconfig.me`)\n  with `dig +short <fqdn>` (run it from the box AND ideally your laptop). If they don't match,\n  **stop** — Caddy's ACME challenge will fail. Have the user create the A record, wait for it\n  to propagate, then continue.\n- Ports **80 and 443** must be reachable from the internet. Check the host firewall AND any\n  cloud security group / network firewall (Hetzner Cloud, AWS SG, etc.) — these are outside\n  the box and a common silent blocker.\n\n### 2. Install Docker (if absent)\n- Check `docker --version` and `docker compose version`. If missing, install Docker Engine +\n  the Compose plugin (Docker's official `get.docker.com` script on Ubuntu/Debian is fine).\n  Re-check `docker compose version` before proceeding.\n\n### 3. Lay down the project\n- Pick `DATA_FOLDER` — an **absolute path**, e.g. `/opt/n8n`. The `DATA_FOLDER` value in `.env`\n  **must equal this exact directory** (the compose mounts `${DATA_FOLDER}/caddy_config/Caddyfile`,\n  and `init-data.sh` is mounted via a relative `./` path), so always run `docker compose` from\n  here. Create it, plus `caddy_config/` and `local_files/` inside.\n- **Get the template files onto the box.** They live in this skill's `assets/` on *your* machine,\n  not on the server — transfer each one. Either `scp` them up, or (no local copy needed) write\n  each file's contents over SSH, e.g.\n  `ssh <target> 'cat > <DATA_FOLDER>/docker-compose.yml' < assets/docker-compose.single.yml`.\n  Land them with these exact names:\n  - the chosen compose → `<DATA_FOLDER>/docker-compose.yml` (rename it to exactly this)\n  - `Caddyfile` → `<DATA_FOLDER>/caddy_config/Caddyfile`\n  - **queue only:** `init-data.sh` → `<DATA_FOLDER>/init-data.sh`, then `chmod +x` it\n  - the matching `.env.*.example` → `<DATA_FOLDER>/.env`\n\n### 4. Fill `.env` + generate secrets\n- Set `DATA_FOLDER`, `DOMAIN_NAME`, `SUBDOMAIN`, `SSL_EMAIL`, `GENERIC_TIMEZONE`.\n- Generate each secret **on the box** with `openssl` (`SECURITY.md` has the commands) and **write\n  it into `.env`, replacing the matching `REPLACE_WITH_…` placeholder**: `N8N_ENCRYPTION_KEY`;\n  queue also `POSTGRES_PASSWORD` + `POSTGRES_NON_ROOT_PASSWORD`.\n- **Before launching, confirm none are left unset:** `grep REPLACE_WITH_ .env` must return nothing\n  — a leftover placeholder becomes the literal password and Postgres/n8n fail to connect.\n- `chmod 600 .env`. Record the encryption key so the user can back it up off-box.\n\n### 5. Firewall\n- `ufw`: allow OpenSSH + 80 + 443, then enable. Do **not** open 5678/5432/6379.\n\n### 6. Launch\n- `cd <DATA_FOLDER> && docker compose up -d`.\n- Queue mode brings up Redis + Postgres + main + workers (workers via `replicas`). To add\n  capacity: `docker compose up -d --scale n8n-worker=N`.\n\n### 7. Verify (don't declare success without this)\n- `docker compose ps` — every service `Up`/healthy (queue: postgres & redis `healthy` first).\n- **n8n itself up (internal):** `docker compose exec n8n wget -qO- http://localhost:5678/healthz`\n  → `{\"status\":\"ok\"}`. This separates \"n8n is running\" from \"TLS isn't ready yet.\"\n- **Cert issued:** `docker compose logs caddy | grep -i 'certificate obtained'`. First-boot ACME\n  can take a minute or two; until it finishes, a public `https://` request fails TLS — that means\n  the cert is still pending, **not** that n8n is down.\n- **Public reachability (with retry):** `curl -fsS --retry 5 --retry-delay 10 https://<fqdn>/healthz`\n  → `{\"status\":\"ok\"}`. (`/healthz` only proves the process is reachable; `/healthz/readiness`\n  additionally confirms the DB is connected and migrated — use it when debugging a boot loop.)\n- **Queue mode — main and workers must agree.** Diff their environments:\n  `diff <(docker compose exec -T n8n env | sort) <(docker compose exec -T --index 1 n8n-worker env | sort)`.\n  Only the public-URL/proxy vars should differ. Anything else means a behavioural setting reached\n  the main but not the workers — and workers are what execute workflows, so it fails at runtime\n  in one node rather than at boot. `QUEUE_MODE.md` explains the rule.\n- Open `https://<fqdn>` → the **owner setup** screen. **Whoever completes that signup form first\n  claims the instance** — an exposed un-owned instance is a race, so create the owner account\n  immediately, before sharing the URL. Enable 2FA. (Automated deploys can pre-provision the\n  owner via env vars instead — see the owner row in `SECURITY.md`.)\n\n### 8. Hand off\n- Give the user: the URL, where the project lives, the encryption key to store safely, and the\n  Day-2 basics (update / backup / restore) from **`DAY2.md`**.\n\n## What NOT to do\n\n- **Don't skip the DNS/ports preflight.** A wrong A record or a closed cloud firewall is the\n  #1 reason Caddy can't get a cert and n8n looks \"broken.\"\n- **Don't publish 5678/5432/6379** to the host. Caddy reaches n8n over the private network.\n- **Don't reuse another instance's encryption key or `.env`.** Fresh secrets per box.\n- **Don't run queue mode on SQLite.** Queue requires Postgres (the template already wires it).\n- **Don't put secrets in `docker-compose.yml` or the Caddyfile.** `.env` only.\n- **Don't add a behavioural env var to the main only (queue mode).** Modules, DB, queue,\n  binary-data and encryption settings belong in the shared `x-n8n-env` anchor so workers get\n  them too; only the public-URL/proxy vars are main-only. See `QUEUE_MODE.md`.\n- **Don't use `:latest` blindly.** Pin `N8N_IMAGE_TAG`; update deliberately (`DAY2.md`).\n\n## Reference files\n\n- **`SINGLE_MODE.md`** — single-instance specifics, SQLite vs Postgres, when to graduate to queue.\n- **`QUEUE_MODE.md`** — queue architecture, workers/concurrency/scaling, shared encryption key, the main-vs-worker **env-parity rule**, optional backend modules (`N8N_ENABLED_MODULES`, e.g. Agents), binary data (`database` mode — filesystem is unsupported in queue mode; S3/Azure = Enterprise), webhook processors, multi-main licensing.\n- **`SECURITY.md`** — generating secrets, the encryption-key rules, the full hardening checklist (telemetry off, env-access block, public API, firewall, secure cookies).\n- **`CREDENTIAL_OVERWRITES.md`** — managed OAuth: register one OAuth app instance-wide so users\n  never see a client ID/secret (\"Sign in with Google\" on self-hosted). The endpoint-vs-env choice,\n  the **mandatory** endpoint auth token, parent-type inheritance, persistence and worker reload.\n- **`DAY2.md`** — changing a setting (env var) safely, updating the image, backing up (encryption key + volume + Postgres), and restoring.\n- **`assets/`** — the templates: `docker-compose.single.yml`, `docker-compose.queue.yml`, `Caddyfile`, `.env.single.example`, `.env.queue.example`, `init-data.sh`.\n\nAuthoritative upstream reference: the official hosting docs live at\n<https://docs.n8n.io/deploy/host-n8n> (restructured mid-2026 from the old `/hosting/` paths —\nprefer these URLs). The env-var reference index is at\n<https://docs.n8n.io/deploy/host-n8n/configure-n8n/basic-configuration/use-environment-variables>.\nWhen this skill and the live docs disagree, trust the docs and tell the user.\n","data/skills/n8n-validation-expert/SKILL.md":"# n8n-validation-expert\n\nPath: `data/skills/n8n-validation-expert/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-validation-expert/SKILL.md)","data/skills/n8n-workflow-patterns/SKILL.md":"# n8n-workflow-patterns\n\nPath: `data/skills/n8n-workflow-patterns/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-workflow-patterns/SKILL.md)","data/skills/using-n8n-mcp-skills/SKILL.md":"# using-n8n-mcp-skills\n\nPath: `data/skills/using-n8n-mcp-skills/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/using-n8n-mcp-skills/SKILL.md)","AGENTS.md":"# AI Agent Protocol & Instructions\n\nPath: `AGENTS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/AGENTS.md)","CLAUDE.md":"# Claude Agent Guidelines & System Prompt\n\nPath: `CLAUDE.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/CLAUDE.md)",".claude/agents/code-reviewer.md":"# Subagent: code-reviewer\n\nPath: `.claude/agents/code-reviewer.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/.claude/agents/code-reviewer.md)",".claude/agents/context-manager.md":"# Subagent: context-manager\n\nPath: `.claude/agents/context-manager.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/.claude/agents/context-manager.md)",".claude/agents/debugger.md":"# Subagent: debugger\n\nPath: `.claude/agents/debugger.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/.claude/agents/debugger.md)",".claude/agents/deployment-engineer.md":"# Subagent: deployment-engineer\n\nPath: `.claude/agents/deployment-engineer.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/.claude/agents/deployment-engineer.md)",".claude/agents/mcp-backend-engineer.md":"# Subagent: mcp-backend-engineer\n\nPath: `.claude/agents/mcp-backend-engineer.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/.claude/agents/mcp-backend-engineer.md)",".claude/agents/n8n-mcp-tester.md":"# Subagent: n8n-mcp-tester\n\nPath: `.claude/agents/n8n-mcp-tester.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/.claude/agents/n8n-mcp-tester.md)",".claude/agents/technical-researcher.md":"# Subagent: technical-researcher\n\nPath: `.claude/agents/technical-researcher.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/.claude/agents/technical-researcher.md)",".claude/agents/test-automator.md":"# Subagent: test-automator\n\nPath: `.claude/agents/test-automator.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/.claude/agents/test-automator.md)","data/skills/n8n-agents/CHAT_AGENT_PATTERNS.md":"# N8n-agents Skill\n\nPath: `data/skills/n8n-agents/CHAT_AGENT_PATTERNS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-agents/CHAT_AGENT_PATTERNS.md)","data/skills/n8n-agents/EXAMPLES.md":"# N8n-agents Skill\n\nPath: `data/skills/n8n-agents/EXAMPLES.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-agents/EXAMPLES.md)","data/skills/n8n-agents/HUMAN_REVIEW.md":"# N8n-agents Skill\n\nPath: `data/skills/n8n-agents/HUMAN_REVIEW.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-agents/HUMAN_REVIEW.md)","data/skills/n8n-agents/MEMORY.md":"# N8n-agents Skill\n\nPath: `data/skills/n8n-agents/MEMORY.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-agents/MEMORY.md)","data/skills/n8n-agents/RAG.md":"# N8n-agents Skill\n\nPath: `data/skills/n8n-agents/RAG.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-agents/RAG.md)","data/skills/n8n-agents/README.md":"# N8n-agents Skill\n\nPath: `data/skills/n8n-agents/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-agents/README.md)","data/skills/n8n-agents/STRUCTURED_OUTPUT.md":"# N8n-agents Skill\n\nPath: `data/skills/n8n-agents/STRUCTURED_OUTPUT.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-agents/STRUCTURED_OUTPUT.md)","data/skills/n8n-agents/SUBWORKFLOW_AS_TOOL.md":"# N8n-agents Skill\n\nPath: `data/skills/n8n-agents/SUBWORKFLOW_AS_TOOL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-agents/SUBWORKFLOW_AS_TOOL.md)","data/skills/n8n-agents/SYSTEM_PROMPT.md":"# N8n-agents Skill\n\nPath: `data/skills/n8n-agents/SYSTEM_PROMPT.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-agents/SYSTEM_PROMPT.md)","data/skills/n8n-agents/TOOLS.md":"# N8n-agents Skill\n\nPath: `data/skills/n8n-agents/TOOLS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-agents/TOOLS.md)","data/skills/n8n-binary-and-data/AGENT_TOOL_BINARY.md":"# N8n-binary-and-data Skill\n\nPath: `data/skills/n8n-binary-and-data/AGENT_TOOL_BINARY.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-binary-and-data/AGENT_TOOL_BINARY.md)","data/skills/n8n-binary-and-data/BINARY_BASICS.md":"# N8n-binary-and-data Skill\n\nPath: `data/skills/n8n-binary-and-data/BINARY_BASICS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-binary-and-data/BINARY_BASICS.md)","data/skills/n8n-binary-and-data/CDN_REQUIREMENT.md":"# N8n-binary-and-data Skill\n\nPath: `data/skills/n8n-binary-and-data/CDN_REQUIREMENT.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-binary-and-data/CDN_REQUIREMENT.md)","data/skills/n8n-binary-and-data/MERGE_FOR_CONTEXT.md":"# N8n-binary-and-data Skill\n\nPath: `data/skills/n8n-binary-and-data/MERGE_FOR_CONTEXT.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-binary-and-data/MERGE_FOR_CONTEXT.md)","data/skills/n8n-binary-and-data/README.md":"# N8n-binary-and-data Skill\n\nPath: `data/skills/n8n-binary-and-data/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-binary-and-data/README.md)","data/skills/n8n-code-javascript/BUILTIN_FUNCTIONS.md":"# N8n-code-javascript Skill\n\nPath: `data/skills/n8n-code-javascript/BUILTIN_FUNCTIONS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-code-javascript/BUILTIN_FUNCTIONS.md)","data/skills/n8n-code-javascript/COMMON_PATTERNS.md":"# N8n-code-javascript Skill\n\nPath: `data/skills/n8n-code-javascript/COMMON_PATTERNS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-code-javascript/COMMON_PATTERNS.md)","data/skills/n8n-code-javascript/DATA_ACCESS.md":"# N8n-code-javascript Skill\n\nPath: `data/skills/n8n-code-javascript/DATA_ACCESS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-code-javascript/DATA_ACCESS.md)","data/skills/n8n-code-javascript/ERROR_PATTERNS.md":"# N8n-code-javascript Skill\n\nPath: `data/skills/n8n-code-javascript/ERROR_PATTERNS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-code-javascript/ERROR_PATTERNS.md)","data/skills/n8n-code-javascript/README.md":"# N8n-code-javascript Skill\n\nPath: `data/skills/n8n-code-javascript/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-code-javascript/README.md)","data/skills/n8n-code-python/COMMON_PATTERNS.md":"# N8n-code-python Skill\n\nPath: `data/skills/n8n-code-python/COMMON_PATTERNS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-code-python/COMMON_PATTERNS.md)","data/skills/n8n-code-python/DATA_ACCESS.md":"# N8n-code-python Skill\n\nPath: `data/skills/n8n-code-python/DATA_ACCESS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-code-python/DATA_ACCESS.md)","data/skills/n8n-code-python/ERROR_PATTERNS.md":"# N8n-code-python Skill\n\nPath: `data/skills/n8n-code-python/ERROR_PATTERNS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-code-python/ERROR_PATTERNS.md)","data/skills/n8n-code-python/README.md":"# N8n-code-python Skill\n\nPath: `data/skills/n8n-code-python/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-code-python/README.md)","data/skills/n8n-code-python/STANDARD_LIBRARY.md":"# N8n-code-python Skill\n\nPath: `data/skills/n8n-code-python/STANDARD_LIBRARY.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-code-python/STANDARD_LIBRARY.md)","data/skills/n8n-code-tool/ERROR_PATTERNS.md":"# N8n-code-tool Skill\n\nPath: `data/skills/n8n-code-tool/ERROR_PATTERNS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-code-tool/ERROR_PATTERNS.md)","data/skills/n8n-code-tool/INPUT_SCHEMA.md":"# N8n-code-tool Skill\n\nPath: `data/skills/n8n-code-tool/INPUT_SCHEMA.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-code-tool/INPUT_SCHEMA.md)","data/skills/n8n-code-tool/README.md":"# N8n-code-tool Skill\n\nPath: `data/skills/n8n-code-tool/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-code-tool/README.md)","data/skills/n8n-error-handling/API_WORKFLOWS.md":"# N8n-error-handling Skill\n\nPath: `data/skills/n8n-error-handling/API_WORKFLOWS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-error-handling/API_WORKFLOWS.md)","data/skills/n8n-error-handling/ERROR_WORKFLOWS.md":"# N8n-error-handling Skill\n\nPath: `data/skills/n8n-error-handling/ERROR_WORKFLOWS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-error-handling/ERROR_WORKFLOWS.md)","data/skills/n8n-error-handling/NODE_ERROR_OUTPUTS.md":"# N8n-error-handling Skill\n\nPath: `data/skills/n8n-error-handling/NODE_ERROR_OUTPUTS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-error-handling/NODE_ERROR_OUTPUTS.md)","data/skills/n8n-error-handling/README.md":"# N8n-error-handling Skill\n\nPath: `data/skills/n8n-error-handling/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-error-handling/README.md)","data/skills/n8n-error-handling/RESPONSE_SHAPES.md":"# N8n-error-handling Skill\n\nPath: `data/skills/n8n-error-handling/RESPONSE_SHAPES.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-error-handling/RESPONSE_SHAPES.md)","data/skills/n8n-expression-syntax/COMMON_MISTAKES.md":"# N8n-expression-syntax Skill\n\nPath: `data/skills/n8n-expression-syntax/COMMON_MISTAKES.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-expression-syntax/COMMON_MISTAKES.md)","data/skills/n8n-expression-syntax/EXAMPLES.md":"# N8n-expression-syntax Skill\n\nPath: `data/skills/n8n-expression-syntax/EXAMPLES.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-expression-syntax/EXAMPLES.md)","data/skills/n8n-expression-syntax/README.md":"# N8n-expression-syntax Skill\n\nPath: `data/skills/n8n-expression-syntax/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-expression-syntax/README.md)","data/skills/n8n-mcp-tools-expert/OPERATIONS_GUIDE.md":"# N8n-mcp-tools-expert Skill\n\nPath: `data/skills/n8n-mcp-tools-expert/OPERATIONS_GUIDE.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-mcp-tools-expert/OPERATIONS_GUIDE.md)","data/skills/n8n-mcp-tools-expert/README.md":"# N8n-mcp-tools-expert Skill\n\nPath: `data/skills/n8n-mcp-tools-expert/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-mcp-tools-expert/README.md)","data/skills/n8n-mcp-tools-expert/SEARCH_GUIDE.md":"# N8n-mcp-tools-expert Skill\n\nPath: `data/skills/n8n-mcp-tools-expert/SEARCH_GUIDE.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-mcp-tools-expert/SEARCH_GUIDE.md)","data/skills/n8n-mcp-tools-expert/VALIDATION_GUIDE.md":"# N8n-mcp-tools-expert Skill\n\nPath: `data/skills/n8n-mcp-tools-expert/VALIDATION_GUIDE.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-mcp-tools-expert/VALIDATION_GUIDE.md)","data/skills/n8n-mcp-tools-expert/WORKFLOW_GUIDE.md":"# N8n-mcp-tools-expert Skill\n\nPath: `data/skills/n8n-mcp-tools-expert/WORKFLOW_GUIDE.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-mcp-tools-expert/WORKFLOW_GUIDE.md)","data/skills/n8n-multi-instance/README.md":"# N8n-multi-instance Skill\n\nPath: `data/skills/n8n-multi-instance/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-multi-instance/README.md)","data/skills/n8n-node-configuration/DEPENDENCIES.md":"# N8n-node-configuration Skill\n\nPath: `data/skills/n8n-node-configuration/DEPENDENCIES.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-node-configuration/DEPENDENCIES.md)","data/skills/n8n-node-configuration/NODE_FAMILY_GOTCHAS.md":"# N8n-node-configuration Skill\n\nPath: `data/skills/n8n-node-configuration/NODE_FAMILY_GOTCHAS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-node-configuration/NODE_FAMILY_GOTCHAS.md)","data/skills/n8n-node-configuration/OPERATION_PATTERNS.md":"# N8n-node-configuration Skill\n\nPath: `data/skills/n8n-node-configuration/OPERATION_PATTERNS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-node-configuration/OPERATION_PATTERNS.md)","data/skills/n8n-node-configuration/README.md":"# N8n-node-configuration Skill\n\nPath: `data/skills/n8n-node-configuration/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-node-configuration/README.md)","data/skills/n8n-self-hosting/CREDENTIAL_OVERWRITES.md":"# N8n-self-hosting Skill\n\nPath: `data/skills/n8n-self-hosting/CREDENTIAL_OVERWRITES.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-self-hosting/CREDENTIAL_OVERWRITES.md)","data/skills/n8n-self-hosting/DAY2.md":"# N8n-self-hosting Skill\n\nPath: `data/skills/n8n-self-hosting/DAY2.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-self-hosting/DAY2.md)","data/skills/n8n-self-hosting/QUEUE_MODE.md":"# N8n-self-hosting Skill\n\nPath: `data/skills/n8n-self-hosting/QUEUE_MODE.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-self-hosting/QUEUE_MODE.md)","data/skills/n8n-self-hosting/README.md":"# N8n-self-hosting Skill\n\nPath: `data/skills/n8n-self-hosting/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-self-hosting/README.md)","data/skills/n8n-self-hosting/SECURITY.md":"# N8n-self-hosting Skill\n\nPath: `data/skills/n8n-self-hosting/SECURITY.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-self-hosting/SECURITY.md)","data/skills/n8n-self-hosting/SINGLE_MODE.md":"# N8n-self-hosting Skill\n\nPath: `data/skills/n8n-self-hosting/SINGLE_MODE.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-self-hosting/SINGLE_MODE.md)","data/skills/n8n-subworkflows/NAMING_AND_DISCOVERY.md":"# N8n-subworkflows Skill\n\nPath: `data/skills/n8n-subworkflows/NAMING_AND_DISCOVERY.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-subworkflows/NAMING_AND_DISCOVERY.md)","data/skills/n8n-subworkflows/README.md":"# N8n-subworkflows Skill\n\nPath: `data/skills/n8n-subworkflows/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-subworkflows/README.md)","data/skills/n8n-subworkflows/SUBWORKFLOW_PATTERNS.md":"# N8n-subworkflows Skill\n\nPath: `data/skills/n8n-subworkflows/SUBWORKFLOW_PATTERNS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-subworkflows/SUBWORKFLOW_PATTERNS.md)","data/skills/n8n-validation-expert/ERROR_CATALOG.md":"# N8n-validation-expert Skill\n\nPath: `data/skills/n8n-validation-expert/ERROR_CATALOG.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-validation-expert/ERROR_CATALOG.md)","data/skills/n8n-validation-expert/FALSE_POSITIVES.md":"# N8n-validation-expert Skill\n\nPath: `data/skills/n8n-validation-expert/FALSE_POSITIVES.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-validation-expert/FALSE_POSITIVES.md)","data/skills/n8n-validation-expert/README.md":"# N8n-validation-expert Skill\n\nPath: `data/skills/n8n-validation-expert/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-validation-expert/README.md)","data/skills/n8n-validation-expert/REVIEW_CHECKLIST.md":"# N8n-validation-expert Skill\n\nPath: `data/skills/n8n-validation-expert/REVIEW_CHECKLIST.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-validation-expert/REVIEW_CHECKLIST.md)","data/skills/n8n-workflow-patterns/README.md":"# N8n-workflow-patterns Skill\n\nPath: `data/skills/n8n-workflow-patterns/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-workflow-patterns/README.md)","data/skills/n8n-workflow-patterns/ai_agent_workflow.md":"# N8n-workflow-patterns Skill\n\nPath: `data/skills/n8n-workflow-patterns/ai_agent_workflow.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-workflow-patterns/ai_agent_workflow.md)","data/skills/n8n-workflow-patterns/database_operations.md":"# N8n-workflow-patterns Skill\n\nPath: `data/skills/n8n-workflow-patterns/database_operations.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-workflow-patterns/database_operations.md)","data/skills/n8n-workflow-patterns/http_api_integration.md":"# N8n-workflow-patterns Skill\n\nPath: `data/skills/n8n-workflow-patterns/http_api_integration.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-workflow-patterns/http_api_integration.md)","data/skills/n8n-workflow-patterns/scheduled_tasks.md":"# N8n-workflow-patterns Skill\n\nPath: `data/skills/n8n-workflow-patterns/scheduled_tasks.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-workflow-patterns/scheduled_tasks.md)","data/skills/n8n-workflow-patterns/webhook_processing.md":"# N8n-workflow-patterns Skill\n\nPath: `data/skills/n8n-workflow-patterns/webhook_processing.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-workflow-patterns/webhook_processing.md)","data/skills/using-n8n-mcp-skills/README.md":"# Using-n8n-mcp-skills Skill\n\nPath: `data/skills/using-n8n-mcp-skills/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/using-n8n-mcp-skills/README.md)"},"files":{"data/skills/n8n-binary-and-data/SKILL.md":"---\nname: n8n-binary-and-data\ndescription: Handle files and binary data in n8n correctly. Use when working with files, images, PDFs, attachments, uploads or downloads, base64, vision/multimodal input, or when an AI agent needs a file as tool input or output — and whenever the user mentions $binary, binaryPropertyName, \"read the PDF\", \"attach the file\", \"send the image\", Merge losing binary, or a CDN for chat images. Covers the $binary vs $json split, reading/writing binary, keeping binary alive across transforms with Merge, the agent-tool binary boundary, and the CDN/URL requirement for chat surfaces.\n---\n\n# n8n Binary and Data\n\nEvery n8n item carries two independent slots: `$json` for structured data and `$binary` for file bytes. They travel side by side through the workflow. File contents — the actual PDF, image, or zip — live in `$binary`, never in `$json`. Get that split wrong and you read an empty field, lose a file mid-flow, or hand an AI agent a tool input it can't use.\n\nThis skill covers where binary lives, how to read and write it, how to keep it from being silently stripped, the hard wall between binary and the AI-agent tool boundary, and why chat surfaces need a URL instead of raw bytes.\n\n---\n\n## The three rules that prevent 90% of binary bugs\n\n1. **File contents are in `$binary`, not `$json`.** After an HTTP download, a \"Read Files\", or an email-attachment trigger, the bytes sit in `$binary.<key>`. `$json` holds metadata at most. Reading `$json.data` for file contents gives you nothing.\n\n2. **Binary cannot cross the AI-agent tool boundary — in either direction.** Tool arguments and tool return values are JSON only. An uploaded image can't be passed into a tool as a file, and a tool can't return raw bytes. Pre-stage to storage and pass a key or URL through JSON instead. See `AGENT_TOOL_BINARY.md`.\n\n3. **Chat surfaces render images by URL, not by `$binary`.** Slack, Discord, Teams, Telegram, embedded webhook chat — none of them read the binary slot. The image has to live somewhere a URL can fetch it. See `CDN_REQUIREMENT.md`.\n\n---\n\n## The two slots\n\nEach item is shaped like this:\n\n```json\n{\n  \"json\": { \"customerId\": 42, \"status\": \"sent\" },\n  \"binary\": {\n    \"invoice\": {\n      \"data\": \"<base64-encoded bytes>\",\n      \"mimeType\": \"application/pdf\",\n      \"fileName\": \"invoice-42.pdf\",\n      \"fileExtension\": \"pdf\"\n    }\n  }\n}\n```\n\nThe key inside `binary` (`invoice` here) is the **binary property name**. Most file-handling nodes have a `binaryPropertyName` parameter that points at it — the producer names the slot, the consumer references it by that name. The default key across most nodes is `data`, so when nothing tells you otherwise, assume `$binary.data`.\n\n`$json` and `$binary` are separate namespaces. An expression like `{{ $binary.invoice.fileName }}` reads file metadata; `{{ $json.customerId }}` reads data. They never mix.\n\nThis split also explains a webhook gotcha: a Webhook trigger receiving `multipart/form-data` puts the uploaded file in `$binary` and the accompanying form fields in `$json.body` — so an uploaded file is not somewhere under `$json` at all. (The `$json.body` nesting for webhooks is **n8n-expression-syntax** territory.)\n\nSee `BINARY_BASICS.md` for the full slot anatomy, mime types, and size limits.\n\n---\n\n## Producing binary\n\nYou rarely build a `$binary` slot by hand — nodes populate it for you:\n\n| Source | How binary appears |\n|---|---|\n| HTTP Request with `responseFormat: \"file\"` | Response body lands in `$binary.data` (or the name you set) |\n| Read/Write Files from Disk | File contents read into `$binary` |\n| Storage downloads (S3, Google Drive, Dropbox, etc.) | Downloaded file in `$binary.<key>` |\n| Email triggers with attachments | Each attachment arrives in `$binary` |\n| Provider AI media nodes (image/audio gen) | Set `options.binaryPropertyOutput` so the bytes land where the next node looks |\n\nFor an HTTP download, the one field that matters is `responseFormat`. Confirm it with `get_node` on `nodes-base.httpRequest` — leaving it as the default JSON/string format is the classic reason a downloaded file ends up as garbled text in `$json` instead of clean bytes in `$binary`.\n\n---\n\n## Reading and writing binary in a Code node\n\nMost workflows never need to crack open the bytes — they just pass binary through to a consumer (email attachment, file upload, Slack file). When you do need the raw bytes, do it in a Code node.\n\n**Read** with `getBinaryDataBuffer` — do not try to base64-decode `$binary.<key>.data` by hand:\n\n```javascript\n// Code node, \"Run Once for Each Item\"\nconst buffer = await this.helpers.getBinaryDataBuffer(0, 'data'); // (itemIndex, propertyName)\nconst text = buffer.toString('utf-8');\nconst length = buffer.length;\n\nreturn [{\n  json: { ...$json, length },\n  binary: $input.item.binary,   // pass the binary through, or it's gone\n}];\n```\n\n**Write** by building the slot yourself — base64 the bytes plus a mime type and file name:\n\n```javascript\nconst text = 'Hello, world!';\nreturn [{\n  json: { ok: true },\n  binary: {\n    report: {\n      data: Buffer.from(text).toString('base64'),\n      mimeType: 'text/plain',\n      fileName: 'report.txt',\n      fileExtension: 'txt',\n    },\n  },\n}];\n```\n\nThe Code-node sandbox, helpers, and execution modes are the domain of **n8n-code-javascript** (and **n8n-code-python**) — use those for the language-level detail. The one binary-specific thing to remember here: a Code node that returns `[{ json: {...} }]` without re-attaching `binary` **silently drops the file**. See `BINARY_BASICS.md`.\n\n---\n\n## Keeping binary alive across transforms\n\nJSON-only nodes — Edit Fields (Set), Code, IF, and others — can drop the `$binary` slot from their output. The workflow validates clean and runs without error; the file just isn't there downstream when the email node goes to attach it.\n\nTwo ways to keep it:\n\n- **Pass-through option on the transforming node.** Edit Fields has `includeOtherFields`; a Code node can return `binary: $input.item.binary` explicitly. Cheapest fix when it's available.\n- **Fan out and Merge by position.** Route the source into both the transform and a bypass branch, then recombine with a Merge in `combineByPosition` mode. The JSON comes from the transform side, the binary survives on the bypass side.\n\n```\n[Source with binary] ─┬─→ [Edit Fields: change JSON] ─┐\n                      │      (binary stripped here)     ├─→ [Merge: combineByPosition] ─→ [Email: attach]\n                      └──────────────────────────────────┘\n                          (bypass — binary passes through untouched)\n```\n\n`combineByPosition` pairs item N from each input, so the field counts must line up. The connection wiring and the alternatives for many-strip-point chains (upload-early, sub-workflow) are in `MERGE_FOR_CONTEXT.md`.\n\n---\n\n## The agent-tool binary boundary\n\nThis is the sharpest edge. An AI Agent talks to its tools (Custom Code Tool, Call n8n Workflow Tool, HTTP Request Tool, MCP tools) over JSON. Binary does not fit through that pipe in either direction. The fix is the same shape both ways: **stage the bytes in storage, pass a key/URL through JSON, fetch on the other side.**\n\n**Inbound — a user uploads a file the agent's tool must operate on:**\n\n1. The chat trigger gives you a `files[]` array. Split it out and upload each file to private storage under a hashed key.\n2. Re-merge that branch before the agent runs (it's a synchronization barrier, not decoration), and set `executeOnce: true` on the agent so N files don't trigger N agent runs.\n3. Inject the keys into the agent's system prompt, listing both the original name (human context) and the storage key (what the tool needs), with an explicit \"use EXACTLY this key\".\n4. The tool receives the key as a string argument and downloads the file from storage itself.\n\n**Outbound — a tool generates a file the agent must return:**\n\n1. The tool sub-workflow generates the binary, uploads it to storage, and returns JSON like `{ \"ok\": true, \"key\": \"...\", \"url\": \"https://...\", \"mimeType\": \"image/png\" }`.\n2. The agent embeds the URL in its reply (or passes the key to another tool).\n\n`passthroughBinaryImages: true` on the agent only changes what the **LLM sees** for vision — it does **not** let tools receive the file, and it's image-only (no PDFs, audio, or video). You still need the upload-and-pass-key pattern for any tool. Full patterns, hash strategy, storage choices, and the long-running-tool variant are in `AGENT_TOOL_BINARY.md`.\n\n> Building the tool itself? See **n8n-code-tool** for the Custom Code Tool contract and **n8n-workflow-patterns** for the AI-Agent-with-tools shape.\n\n---\n\n## The CDN requirement for chat surfaces\n\nWhen a workflow generates an image and the user wants it shown inside a chat message:\n\n- **Binary on the item isn't enough.** The chat client renders messages that reference images by URL (or pushes bytes through the platform's own file-upload API). It never reads `$binary`.\n- **The bytes have to live somewhere a URL can fetch over HTTPS.** Upload to an object store or drive first, then embed the returned URL.\n- **n8n has no built-in CDN.** The user provides the storage.\n\nAsk which storage they already use rather than defaulting to S3 — object storage (S3, R2, GCS, Azure Blob, Backblaze B2, Supabase Storage) and drive-style services (Dropbox, Google Drive, OneDrive, Box) all work and all change the URL shape. Cloudflare R2 is the lowest-friction starting point if they have nothing. For sensitive content, use a signed URL with an expiry rather than a permanently public one. See `CDN_REQUIREMENT.md`.\n\n---\n\n## What's NOT available\n\n- **`$fromAI()` cannot carry binary.** It fills tool parameters with strings, numbers, booleans, and objects — never file bytes. Pass a storage key instead.\n- **Tool arguments and returns are JSON only.** There is no \"binary parameter\" on an agent tool, in or out.\n- **n8n ships no CDN or public file host.** Serving a file over a URL is always something the user's storage does, not n8n.\n- **`getBinaryDataBuffer` is a Code-node helper.** It isn't available in the Custom Code Tool sandbox (see **n8n-code-tool**).\n\n---\n\n## Where Data Tables live\n\nFor persistent tabular storage — reference-counting staged files, tracking which keys are live, dedup — that's the `n8n_manage_datatable` surface, owned by **n8n-mcp-tools-expert**. This skill does not cover Data Tables.\n\n---\n\n## Anti-patterns\n\n| Anti-pattern | What goes wrong | Fix |\n|---|---|---|\n| Reading file contents from `$json` | Bytes live in `$binary`; `$json` is empty or metadata only | Read `$binary.<key>`, or `getBinaryDataBuffer` in a Code node |\n| HTTP download without `responseFormat: \"file\"` | Bytes arrive as mangled text in `$json`, not clean binary | Set `responseFormat: \"file\"` on the HTTP Request node |\n| Code node returns `[{json:{...}}]`, no `binary` | The file is silently dropped downstream | Re-attach `binary: $input.item.binary` in the return |\n| JSON transform (Edit Fields/IF) eats the binary | Email/upload node finds nothing to attach | Pass-through option, or fan out + Merge by position |\n| Passing an uploaded file into a tool via `$fromAI` | `$fromAI` can't carry binary; the tool gets nothing | Pre-stage to storage, inject the key in the system prompt, tool fetches by key |\n| Assuming `passthroughBinaryImages` lets tools see the file | It only affects what the LLM sees, and only for images | Still need the upload-and-pass-key pattern for tools |\n| Tool returns raw binary to the agent | Tool output is JSON; bytes don't survive (and bloat context) | Upload, return `{ key, url }` in JSON |\n| Posting `$binary` to a chat surface and expecting an image | Chat clients render by URL, not raw bytes | Upload to storage/CDN, embed the URL or use the platform file API |\n| Hardcoding base64 in a Code node | Huge workflow JSON, slow, leaky | Reference via `$binary`, or upload and reference by URL |\n\n---\n\n## Reference files\n\n| File | Read when |\n|---|---|\n| `BINARY_BASICS.md` | First time handling binary, or reading/writing the `$binary` slot, mime types, size limits |\n| `AGENT_TOOL_BINARY.md` | An agent tool needs an uploaded file, or produces one — the boundary in either direction |\n| `MERGE_FOR_CONTEXT.md` | Binary disappears after a JSON transform and you need to re-attach it |\n| `CDN_REQUIREMENT.md` | Showing images in a chat surface or anywhere that needs URL-referenced images |\n\n---\n\n## Integration with Other Skills\n\n**n8n-code-javascript / n8n-code-python**: the Code node is where you read/write raw bytes (`getBinaryDataBuffer`, `Buffer.from(...).toString('base64')`). Those skills own the sandbox, helpers, and execution-mode detail — this skill owns the rule that binary must be re-attached on return.\n\n**n8n-code-tool**: the Custom Code Tool sandbox is narrower — no `$binary`, no `getBinaryDataBuffer`, no `$fromAI`. When a tool needs a file, this skill's storage-key pattern is how it gets one.\n\n**n8n-workflow-patterns**: the agent-tool binary boundary sits inside the AI-Agent-with-tools pattern; the CDN flow is a generate → upload → reply chain.\n\n**n8n-node-configuration**: `responseFormat`, `binaryPropertyName`, `includeOtherFields`, `binaryPropertyOutput` are all conditional fields — use `get_node` to confirm the exact names on the user's version.\n\n**n8n-expression-syntax**: addressing `$binary.<key>.fileName` vs `$json.body` (webhook uploads in particular) is expression territory.\n\n**n8n-validation-expert**: a dropped binary slot is a silent failure — `validate_workflow` won't flag it. Confirm presence by inspecting the execution.\n\n**n8n-mcp-tools-expert**: owns `n8n_manage_datatable` (Data Tables) and `n8n_executions` — use the latter to confirm a `binary` slot actually survived a given node.\n\n**n8n-error-handling**: storage uploads and downloads fail; the inbound/outbound staging steps need error branches so a missing key doesn't 404 silently.\n\n**using-n8n-mcp-skills**: the index of how these skills fit together.\n\n---\n\n## Verifying binary survived\n\nValidation won't catch a stripped binary slot — it's a silent failure. Confirm it ran correctly:\n\n1. `n8n_test_workflow` (or trigger a real run) to produce an execution.\n2. `n8n_executions` to pull that execution, and inspect per-node output for the `binary` slot — it shows presence and metadata even if the base64 is too large to render.\n3. The node where `binary` last appears is the node before the strip. That's where the pass-through or Merge goes.\n\n---\n\n## Quick Reference Checklist\n\n- [ ] File contents read from `$binary.<key>` — never `$json`\n- [ ] HTTP downloads use `responseFormat: \"file\"`\n- [ ] Code nodes re-attach `binary` on return when the file must continue\n- [ ] JSON transforms either pass binary through or Merge it back (`combineByPosition`)\n- [ ] No attempt to pass binary into/out of an agent tool — keys/URLs through JSON instead\n- [ ] `passthroughBinaryImages` used only for LLM vision, not as a tool channel\n- [ ] Chat-surface images uploaded to storage; the URL is embedded, not the bytes\n- [ ] Storage backend chosen with the user (not defaulted to S3); signed URLs for sensitive content\n- [ ] Binary presence confirmed by inspecting the execution, not by validation\n\n---\n\n**Remember**: two slots, side by side. Data rides in `$json`, files ride in `$binary` — and the moment a file has to cross an agent tool or reach a chat surface, it travels as a URL, not as bytes.\n","data/skills/n8n-agents/SKILL.md":"---\nname: n8n-agents\ndescription: Design n8n AI agents the right way. Use when building or editing any @n8n/n8n-nodes-langchain.* AI node — an AI Agent, LLM chain, Text Classifier, or Information Extractor — and whenever the user mentions AI agents, LLM with tools, tool calling, $fromAI, system prompts, agent memory, sessionId, structured/JSON output, output parser, RAG, vector store, a chat assistant/bot, or human-in-the-loop review. Covers Agent-vs-chain-vs-classifier choice, the model/memory/tools/outputParser slots, tool names/descriptions as prompt, structured output with autoFix, memory, RAG, human review, and chat topologies.\n---\n\n# n8n Agents\n\nThe n8n AI Agent node (`@n8n/n8n-nodes-langchain.agent`) is a multi-turn LLM driver with sub-nodes for the model, memory, tools, and an optional output parser. This skill is the **deep** guide to designing agents and the LangChain family around them. For the high-level \"where an agent fits in a workflow\" picture, see **n8n-workflow-patterns** `ai_agent_workflow.md` — this skill goes one level down into *how to build it well*.\n\nFor node-type formats: in workflow JSON the LangChain nodes use the long `@n8n/n8n-nodes-langchain.*` form (`.agent`, `.lmChatOpenAi`, `.memoryBufferWindow`, `.outputParserStructured`, `.toolWorkflow`, `.toolHttpRequest`, `.toolCode`). When you call `get_node` / `validate_node`, use the **short** form (`nodes-langchain.agent`). See **n8n-mcp-tools-expert** for the format rules.\n\n---\n\n## Pick the right node first\n\nReaching for an Agent when the task is one-shot classification or extraction is the most common over-build. Decide before you wire anything:\n\n| You need to… | Use | Why |\n|---|---|---|\n| Call tools, reason over multiple turns, or hold memory | **AI Agent** (`.agent`) | The full loop: model + tools + memory + optional parser. Also a fine default when you'd rather standardize. |\n| One-shot text in → text out, no tools | **Basic LLM Chain** (`.chainLlm`) | No agent loop, easier to debug. Still accepts an `outputParserStructured` sub-node. |\n| Route a natural-language input to one of **N branches** | **Text Classifier** (`.textClassifier`) | ONE node, N output handles, downstream wires directly into each. Not Agent + Switch. |\n| Pull structured fields out of free text | **Information Extractor** (`.informationExtractor`) | Purpose-built field extraction with a schema. |\n| 3-way positive/neutral/negative split | **Sentiment Analysis** (`.sentimentAnalysis`) | Built-in branch outputs. |\n| Condense a long document | **Summarization Chain** (`.chainSummarization`) | Map-reduce summarization built in. |\n| Generate an image / audio / video | **The provider's native single-call node** (OpenAI, Gemini, ElevenLabs…) | NEVER wrap media generation in an Agent — see \"Binary and the agent boundary\". |\n\n**Text Classifier detail (the Agent + Switch anti-pattern):** every category needs both a **name AND a description**. The model routes against the *description*, not the name — a category with no description gets picked by coin-flip. Set `options.enableAutoFixing: true` for robustness on edge inputs. One node, N branches, done. Reaching for an Agent that \"decides\" then a Switch that \"routes\" is two nodes plus prompt boilerplate for what Text Classifier does natively.\n\nChat-model nodes (`.lmChatOpenAi`, `.lmChatAnthropic`, `.lmChatOpenRouter`, …) are **sub-nodes** — they don't run standalone. They wire into a chain, agent, classifier, or extractor via the `ai_languageModel` connection.\n\n---\n\n## The sub-node pattern\n\nThe Agent has a **main input** (the prompt / user message) and up to four **sub-node slots**, each wired by its own `ai_*` connection type:\n\n| Slot | Connection type | Required? | Node example |\n|---|---|---|---|\n| **model** | `ai_languageModel` | Yes | `.lmChatOpenAi`, `.lmChatAnthropic`, `.lmChatOpenRouter` |\n| **memory** | `ai_memory` | Optional | `.memoryBufferWindow`, `.memoryPostgresChat` |\n| **tools** | `ai_tool` | Optional (but the point of an agent) | `slackTool`, `.toolWorkflow`, `.toolHttpRequest`, `.toolCode` |\n| **outputParser** | `ai_outputParser` | Optional | `.outputParserStructured` |\n\nA sub-node connects FROM itself TO the agent. In workflow JSON the connection lives on the **sub-node**, keyed by the `ai_*` type:\n\n```json\n\"Main LLM\": {\n  \"ai_languageModel\": [[{ \"node\": \"AI Agent\", \"type\": \"ai_languageModel\", \"index\": 0 }]]\n},\n\"Simple Memory\": {\n  \"ai_memory\": [[{ \"node\": \"AI Agent\", \"type\": \"ai_memory\", \"index\": 0 }]]\n},\n\"Search customer DB\": {\n  \"ai_tool\": [[{ \"node\": \"AI Agent\", \"type\": \"ai_tool\", \"index\": 0 }]]\n}\n```\n\nMultiple tools all connect into the same `ai_tool` index 0 — they stack, they don't fan into separate indices. With `n8n_update_partial_workflow` you wire each with an `addConnection` op using `sourceOutput: \"ai_tool\"`. The agent puts its final answer in **`$json.output`** (not `.text`, not `.response`) — downstream nodes read `{{ $json.output }}`.\n\nSee **EXAMPLES.md** for a complete stateless agent-core node-object snippet.\n\n---\n\n## Two non-negotiables\n\n1. **Tool names and descriptions ARE part of the prompt.** The model picks a tool by reading its name and description — nothing else. A tool named `tool1` with an empty description is invisible to the model: it skips it, mis-selects it, or hallucinates parameters. There's usually no error — just an agent that \"won't use my tool\". Treat both like API design. → **TOOLS.md**\n2. **Structured output must parse AND autoFix.** An `outputParserStructured` with `autoFix: true` and a **coding-capable fixer model** is the production pattern. Without autoFix, one malformed JSON response halts the whole workflow. → **STRUCTURED_OUTPUT.md**\n\n---\n\n## Strong defaults\n\n- **Per-tool usage goes in the tool description, not the system prompt.** Anything about *how to call this specific tool* belongs with the tool, so it travels across agents and keeps the system prompt focused. → **SYSTEM_PROMPT.md**\n- **Sub-workflow tools (`.toolWorkflow`) for anything multi-step.** Any workflow becomes a tool with typed `$fromAI()` inputs, and composes with branching, error handling, and reuse. Default here when in doubt. → **SUBWORKFLOW_AS_TOOL.md** and **n8n-subworkflows**.\n- **Wrap tools with user-visible side effects in human review.** Sends, payments, refunds, account changes get gated behind an approval node so a human signs off before the tool fires. → **HUMAN_REVIEW.md**\n- **Raise `maxIterations`.** The default tool-call cap is **low** (single digits on most versions) — fine for a one-tool agent, far too low for a multi-tool agent that chains several calls per turn. It surfaces as \"max iterations reached\" or empty output. Set `options.maxIterations` to a realistic ceiling (15 for a focused sub-agent, 50-200 for a broad orchestrator).\n- **Put the current date in the system prompt** via `{{ $now }}` (or `{{ $now.format('DDDD') }}`). A hardcoded date is stale immediately.\n\n---\n\n## The four tool types\n\nPick the lightest option that covers the job:\n\n| Tool type | Node | Use when |\n|---|---|---|\n| **Native tool node** | `slackTool`, `gmailTool`, `toolCalculator`, … | The capability maps to one existing node + one operation. Lowest overhead. |\n| **Sub-workflow as tool** | `.toolWorkflow` | More than one node, reusable logic, or you want independent testability. The canonical n8n way — **default when in doubt**. |\n| **HTTP Request Tool** | `.toolHttpRequest` | A single external HTTP API the agent should orchestrate directly. Reuse the service's predefined credential to cover operations a native node doesn't expose. |\n| **MCP Client Tool** | `.mcpClientTool` | A maintained MCP server already covers it, or you want one published workflow to serve many agents. |\n\nThere is also a **Custom Code Tool** (`.toolCode`) for pure inline computation — but its runtime contract (string in / string out, no `$fromAI`, no `$helpers`) is owned by the **n8n-code-tool** skill. Read that before writing one. Rule of thumb: if you find yourself reaching for `$fromAI()` inside the code, you want `.toolWorkflow` instead.\n\n### `$fromAI()`: how the agent fills tool parameters\n\nTool parameters the agent should decide are wrapped in `$fromAI()`. It is a **real n8n expression helper**, used inside a tool node's parameter expressions:\n\n```\n={{ $fromAI('paramName', 'what to put here — be specific: format, range, example', 'string') }}\n```\n\n- **paramName** — the name the model uses internally (snake_case or camelCase, be consistent).\n- **description** — tells the model what value to produce. **It is part of the prompt** — write it like JSDoc.\n- **type** (optional) — `'string'` (default), `'number'`, `'boolean'`, `'json'`. A wrong-typed value fails the call.\n- **defaultValue** (optional) — used when the model omits it.\n\n`$fromAI()` carries JSON only — it **cannot carry binary** (no base64, no file bytes). And not every parameter has to be `$fromAI`: plumb identity, authority limits, and correlation IDs (`userId`, refund caps, `sessionId`) deterministically from workflow context so the agent can't get them wrong or even see them. → **TOOLS.md** for the full anatomy and the \"give the agent a button, not a steering wheel\" pattern.\n\n---\n\n## System prompt vs tool description\n\n| Belongs in the **system prompt** | Belongs in the **tool's description** |\n|---|---|\n| Persona, role, voice | What this specific tool does |\n| Global output/format rules (\"respond in markdown\") | When to use it vs other tools |\n| Refusal / safety behavior | What each parameter means and its shape |\n| Display protocols (`![]()` for images) | Examples of good vs bad invocations |\n| Universal context (current date via `$now`, user role) | Tool-specific gotchas (rate limits, edge cases) |\n| Inter-tool flow (\"after generating, always display\") | Tool-specific input transformations |\n\nWhy split it: a well-described tool works in **any** agent that drops it in, tool details only \"load\" when the model considers that tool (token efficiency), and you update one tool description instead of a paragraph buried in a 5000-token prompt. → **SYSTEM_PROMPT.md**\n\n---\n\n## Structured output: when and how\n\nAdd an `outputParserStructured` sub-node (wired `ai_outputParser`) when downstream needs strict JSON, not free-form text. Two rules:\n\n1. **Use `schemaType: 'manual'` with a real JSON Schema, not `jsonSchemaExample`.** An example can't express required-vs-optional, enums, numeric ranges, or array constraints — you outgrow it the first time the shape gets non-trivial. Reach for `fromJson` + an example only for throwaway shapes.\n2. **`autoFix: true` with a coding-capable fixer model.** Wire a *second* model into the parser's `ai_languageModel` slot. Reconciling broken JSON against a schema is a coding task — a weak fixer just produces another malformed retry and burns tokens.\n\n→ **STRUCTURED_OUTPUT.md** for the schema patterns, the load-bearing \"DO NOT wrap in markdown\" retry line, and the parse-failure cookbook.\n\n---\n\n## Memory: brief mental model\n\nMemory is a sub-node (`ai_memory`). Without it, every call is stateless — correct for one-shot tasks (classify, summarize). With it, the agent holds a conversation, keyed by whatever expression you bind to `sessionKey`.\n\n- **`memoryBufferWindow`** — keeps the last N exchanges per key and persists across executions via n8n's store. The default for chat. **`contextWindowLength` defaults to 5, which is very low** — 50 is a saner starting point. Messages past the window are gone entirely.\n- **`memoryPostgresChat` / `memoryRedisChat`** — only when memory must be read *outside* the agent (your own UI, analytics, cross-system). Not needed just to survive restarts; BufferWindow already does that.\n\n**Plumb a stable key from the trigger to memory consistently.** Chat triggers fill `sessionId` automatically; for other surfaces derive one (Slack `thread_ts`, a webhook conversation ID). Never hardcode `sessionId: 'default'` and never put `sessionId` behind `$fromAI` (the model will fabricate a UUID). → **MEMORY.md**\n\n---\n\n## Binary and the agent boundary\n\nThis is the seam that trips people up:\n\n- **The model CAN see uploaded images** (vision) via `options.passthroughBinaryImages: true` on the agent.\n- **Tools CANNOT receive binary.** `$fromAI()` is JSON-only — no base64, no bytes, even through non-AI bindings.\n- **The agent's output is text-shaped** (or structured-text with a parser). When a model returns image/audio/video bytes, the Agent doesn't surface them at all — there's nothing to recover downstream.\n\n**Workaround:** pre-stage uploads to storage before the agent runs, inject the storage keys into the system prompt, and let tools accept the key as a string parameter and re-fetch internally. For one-shot media generation, skip the agent and call the provider's native single-call node directly.\n\nThe binary mechanics (which storage, how to stage, how to re-fetch) are owned by **n8n-binary-and-data** — see its agent-tool binary reference. This skill only marks the boundary; don't re-derive the mechanics here.\n\n---\n\n## Human review (gate destructive tools)\n\nWhen a tool's effect needs human sign-off before execution (sends, payments, refunds, account changes), wrap it with a review tool node — `slackHitlTool`, `discordHitlTool`, `telegramHitlTool`, `gmailHitlTool`, etc. (n8n names these \"Hitl\" / human-in-the-loop). The review node sits **between** the wrapped tool and the agent on the `ai_tool` connection: wrapped tool → review node → Agent.\n\nWhether sign-off is needed is a product/policy call — **surface the question to the user**, recommend based on blast radius, and let them decide.\n\n**The critical rule: show the actual parameters the wrapped tool will receive.** Use the literal `{{ $tool.parameters.<name> }}` in the approval message, never a `$fromAI()` paraphrase — otherwise the human approves text the model made up, not the call about to fire. → **HUMAN_REVIEW.md**\n\n---\n\n## Chat agents (Slack, Discord, Teams, Telegram)\n\n**The one non-negotiable, regardless of complexity:** any chat-triggered workflow that posts a reply MUST **filter out the bot's own user ID**, or its own replies re-trigger it in an infinite loop that burns runs and tokens. Prefer trigger-level filtering when available (Slack Trigger's `options.userIds` is an **exclusion list** — put the bot ID there); otherwise filter `$json.user !== '<BOT_USER_ID>'` in the first node after the trigger.\n\nBeyond the filter, a simple bot (trigger → agent → reply) lives fine in one workflow. Split into **shell + core + sub-agents** only once you need loading UX, sub-agents, multi-surface reuse, or robust error handling:\n\n- **Shell** — trigger, anti-loop filter, event-type Switch, loading/error UX, renders the reply. No LLM.\n- **Core** — stateless agent, `chatInput` + `threadId` inputs, memory keyed on `threadId`, tools and sub-agents.\n- **Sub-agents** — one narrow domain each, called via `.toolWorkflow`, **stateless** (full context in `chatInput`).\n\n→ **CHAT_AGENT_PATTERNS.md** for per-surface semantics, threading-as-session, and the full topology.\n\n---\n\n## Persisted n8n Agents (n8n_manage_agents)\n\nA **persisted n8n Agent** is a different artifact from the AI Agent node covered above: a standalone assistant record — model, instructions, tools, skills, tasks, memory, channels — stored and versioned by n8n itself, managed through `n8n_manage_agents` (n8n's instance-level MCP server), not a node inside a workflow's JSON.\n\n| You need to… | Use |\n|---|---|\n| A reasoning step inside a workflow, wired with `ai_*` sub-nodes | **AI Agent node** (this skill, above) |\n| A standalone assistant with its own lifecycle — draft, validate, publish, versions, channels — independent of any one workflow | **Persisted Agent** (`n8n_manage_agents`) |\n\n**Prerequisites:** `N8N_MCP_ACCESS_TOKEN` configured (separate from the Public API key) and n8n **2.34+** with the agents module enabled. The token is required for **every** action, including `reference`/`search` — without it, nothing works. Separately, `reference` and `search` work for **any** agent regardless of MCP exposure; every other action needs the target agent exposed to MCP (agents created through this tool are exposed automatically — the exposure gate only matters for agents that already existed before this tool touched them).\n\n**Build sequence:**\n1. `action: \"reference\"` — read the config schema and the exact mutate operations before anything else.\n2. `action: \"discover_assets\"` — list what the agent can actually be wired to. Takes `projectId` (from `n8n_list_catalog({kind: \"projects\"})`) and `kind`: `models` (with a `provider`), `integrations`, `workflows`, `subagents` or `mcpServers`. One call per kind.\n3. `action: \"create\"` — `projectId`, `name`, `config`.\n4. `action: \"mutate\"` — one resource per call (`config.patch`, `skill.upsert`/`delete`, `task.upsert`/`delete`, `customTool.upsert`/`delete`), always carrying the **latest** hash forward. Mind the two names: n8n returns it as `configHash` and expects it back as `args.baseConfigHash`. `args` are forwarded to n8n verbatim, so a near-miss on any field name comes back as `INVALID_ARGS`, not a helpful correction — which is why step 1 reads the schema first. A stale hash comes back as `STALE_CONFIG` — re-`get` and retry with the fresh one.\n5. `action: \"validate\"` — before offering to `call` or `publish`.\n6. `action: \"publish\"` — **only on the user's explicit request**, never proactively.\n\n`action: \"call\"` runs the agent with real credentials and real tools — a live execution, not a dry run. A result can carry `approvals[]` for tool calls that need a human decision; **never approve on the user's behalf** — surface them and resume only after the user decides.\n\n**Custom tools are a third code runtime — don't reuse either of the others.** A `customTool.upsert` body is **TypeScript**, and the only imports it may use are `@n8n/agents` and `zod`. This is not the Code node (JavaScript/Python, returns `[{json: …}]`) and not the AI-agent Custom Code Tool covered by **n8n-code-tool** (`@n8n/n8n-nodes-langchain.toolCode`, returns a string, no `$fromAI()`). Reaching for the wrong contract is the easy mistake here, because all three are \"write code the agent calls\". Read the shape from `action: \"reference\"` before writing one; a compile failure or an unknown `agentId` surfaces as `AGENT_TOOL_ERROR`.\n\n**Credential caveat:** on n8n 2.36.x the agents runtime rejects `azureOpenAiApi` and `aws` credentials (reported as `missing: [\"credential\"]`); the response's `hint` names the accepted types instead.\n\n**Testing without leaving debris:** name throwaway agents `[TEST] …` and `delete` them when you're done — a persisted Agent outlives the conversation that made it, unlike a workflow you can leave inactive.\n\n→ **n8n-mcp-tools-expert** `## Agents` for the tool's full action list and error codes.\n\n---\n\n## RAG (retrieval augmented generation)\n\nn8n ships the LangChain RAG primitives (document loaders, splitters, embeddings, vector stores, retrievers). Two opinions worth stating up front:\n\n1. **Rule out cheaper lookups first.** Exact lookups → a database or Data Table query, not RAG. Freshness → a live search tool. A small/structured doc set → give the agent list/fetch tools. Reach for a vector store only when there are too many docs to list and queries are semantic.\n2. **Wire the vector store as a retrieval tool** (`mode: 'retrieve-as-tool'`, `ai_tool`) so the agent decides when retrieval is relevant and can phrase the query itself. Embed query and documents with the **same** model.\n\n→ **RAG.md** (intentionally thin — defaults depend on data shape and scale).\n\n---\n\n## Reference files\n\n| File | Read when |\n|---|---|\n| **TOOLS.md** | Adding tools, choosing among the four types, writing names/descriptions, `$fromAI` anatomy |\n| **SUBWORKFLOW_AS_TOOL.md** | Wiring a sub-workflow as a tool via `.toolWorkflow`, mapping agent-filled vs plumbed params |\n| **SYSTEM_PROMPT.md** | Writing/refactoring a system prompt, the system-prompt-vs-tool-description split |\n| **STRUCTURED_OUTPUT.md** | Forcing JSON output, configuring autoFix, the fixer model, parse-failure fixes |\n| **MEMORY.md** | Choosing a memory type, persistence, sessionId handling |\n| **HUMAN_REVIEW.md** | Adding human approval, approval-message content, multi-channel approver |\n| **CHAT_AGENT_PATTERNS.md** | Building a Slack/Discord/Teams/Telegram bot, shell + core + sub-agents topology |\n| **RAG.md** | Retrieval-augmented agents (thin by design) |\n| **EXAMPLES.md** | Concrete node-object snippets: stateless agent core, Slack router shell, domain sub-agent |\n\n---\n\n## Anti-patterns\n\n| Anti-pattern | What goes wrong | Fix |\n|---|---|---|\n| Generic tool names (`tool1`, `doStuff`, `runQuery`) | Model can't tell which tool to pick — skips them or hallucinates params | Verb-first specific names: `Search customer database`, `Generate image with Veo` |\n| Empty or one-line tool descriptions | Model has no idea when to invoke; bad selection, no error | Write a real description: what it does, when to use, what each param means |\n| Cramming per-tool instructions into the system prompt | Bloated prompt, no reuse, per-tool guidance buried | Move tool-specific instructions into tool descriptions |\n| Agent + Switch to route on natural language | Two nodes + prompt boilerplate where Text Classifier is one node | Use Text Classifier — each category gets its own output handle (name **and** description) |\n| Wrapping image/audio/video generation in an Agent | Binary doesn't flow through tools or out of the agent output | Use the provider's native single-call node directly |\n| `outputParserStructured` without `autoFix` | One malformed response halts the workflow | `autoFix: true` + a coding-capable fixer model |\n| Passing binary directly to a tool | Doesn't work — binary can't cross the tool boundary | Pre-stage to storage, pass keys; see **n8n-binary-and-data** |\n| Hardcoded `sessionId` / no sessionId / `sessionId` behind `$fromAI` | Conversations cross, or the model fabricates a UUID | Plumb a stable key from the trigger to memory and tools |\n| Two near-identical tools | Selection is non-deterministic, model gets confused | One tool with internal branching driven by a parameter |\n| Chat bot with no bot-user filter | Its own replies re-trigger it → infinite loop | Exclude the bot user ID at the trigger or first node |\n| `maxIterations` left at the low default on a multi-tool agent | \"Max iterations reached\" / empty output | Raise `options.maxIterations` |\n| Filling the human-review message via `$fromAI()` | Approver signs off on a paraphrase, not the real call | Use literal `{{ $tool.parameters.<name> }}` |\n\n---\n\n## What's NOT available via the community MCP\n\n| Want to do | Reality |\n|---|---|\n| Chat-test a workflow's AI Agent node end-to-end interactively | `n8n_test_workflow` runs the workflow, but a true multi-turn chat session against the node is a UI activity (canvas chat tester). A persisted Agent, by contrast, can be run live via `n8n_manage_agents` `call` — see \"Persisted n8n Agents\" above. |\n| Set credentials' actual secret values | `n8n_manage_credentials` creates/updates credential records, but the agent provider keys themselves are entered/verified in the UI. |\n| Assign a workflow's Error Workflow | UI only — see **n8n-error-handling**. Build the catch-all, then hand the user the UI step. |\n| Pin the exact model availability per instance | Model lists shift between versions — `search_nodes`/`get_node` reflect what's installed. Verify on the target instance. |\n\nWhat the MCP **can** do: search and inspect every LangChain node (`search_nodes`, `get_node`), validate node config and the whole graph (`validate_node`, `validate_workflow`), build and patch the agent and its sub-nodes (`n8n_update_partial_workflow` with `addConnection` on `ai_*` outputs), test (`n8n_test_workflow`), and pull the saved JSON to verify wiring (`n8n_get_workflow`). The deep AI-agent guide also lives in `tools_documentation({topic: \"ai_agents_guide\", depth: \"full\"})`.\n\n---\n\n## Integration with other skills\n\n- **n8n-workflow-patterns** (`ai_agent_workflow.md`) — the high-level \"agent in a workflow\" shape. This skill is the deep dive; start there for architecture.\n- **n8n-mcp-tools-expert** — node-type formats (short form for `get_node`, long form in JSON) and tool-selection guidance. Consult before any MCP call.\n- **n8n-node-configuration** — `displayOptions`-driven fields on the agent and sub-nodes; Slack/Block Kit message shapes (`NODE_FAMILY_GOTCHAS.md`, Slack section).\n- **n8n-expression-syntax** — `{{ }}`, `$json.output`, `$now`, and `$fromAI`/`$tool.parameters` all rely on correct expression syntax.\n- **n8n-code-tool** — the Custom Code Tool's runtime contract (string in/out, no `$fromAI`). Read it before writing a `.toolCode`.\n- **n8n-subworkflows** — the sub-workflow primitive that `.toolWorkflow` builds on (Execute Workflow Trigger inputs/outputs, naming, search-before-build).\n- **n8n-binary-and-data** — owns the agent-tool binary boundary mechanics (staging uploads, returning generated files).\n- **n8n-validation-expert** — interpreting `validate_workflow` results, including AI-connection issues (a tool wired into `main` instead of `ai_tool` flags as disconnected).\n- **n8n-error-handling** — `onError: 'continueErrorOutput'` on tool sub-workflows and the agent-core call; error UX on chat shells.\n- **n8n-code-javascript / n8n-code-python** — for Code-node logic *inside* a tool sub-workflow (different sandbox from the Code Tool).\n\n---\n\n## Quick reference checklist\n\nBefore shipping an agent:\n\n- [ ] **Right node**: Agent for tools/memory/multi-turn; Text Classifier for routing; Information Extractor for fields; native node for media\n- [ ] **Model** wired via `ai_languageModel`\n- [ ] **Every tool** has a verb-first specific name AND a real description\n- [ ] **`$fromAI()` descriptions** are specific (format, range, example); identity/limits/sessionId plumbed deterministically, not via `$fromAI`\n- [ ] **Per-tool guidance** lives in tool descriptions, not the system prompt\n- [ ] **`$now`** in the system prompt (no hardcoded date)\n- [ ] **`maxIterations`** raised for multi-tool agents\n- [ ] **Memory** keyed on a stable `sessionKey` from the trigger (not `'default'`, not `$fromAI`); `contextWindowLength` raised from 5\n- [ ] **Structured output**: `schemaType: 'manual'` + `autoFix: true` + a coding-capable fixer model\n- [ ] **Destructive tools** wrapped in human review; approval message uses `$tool.parameters`, not `$fromAI`\n- [ ] **Chat bots** filter the bot's own user ID (trigger-level or first node)\n- [ ] **Binary**: model vision via `passthroughBinaryImages`; tools get storage keys, never bytes\n- [ ] **Validated** with `validate_workflow` and verified with `n8n_get_workflow` (sub-nodes on `ai_*`, not `main`)\n\n---\n\n**Remember**: an agent is only as good as its tool names, descriptions, and system-prompt discipline. The model can't see your wiring — it sees a system prompt and a list of named, described tools. Design those like an API and most \"the agent won't behave\" problems disappear.\n","data/skills/n8n-code-python/SKILL.md":"---\nname: n8n-code-python\ndescription: Write Python code in n8n Code nodes. Use when writing Python in n8n, using _input/_json/_node syntax, working with standard library, or need to understand Python limitations in n8n Code nodes. Use this skill when the user specifically requests Python for an n8n Code node. Note — JavaScript is recommended for 95% of use cases — only use Python when the user explicitly prefers it or the task requires Python-specific standard library capabilities (regex, hashlib, statistics). EXCEPTION — for Python in the AI-agent-callable Custom Code Tool (@n8n/n8n-nodes-langchain.toolCode), use the n8n-code-tool skill instead (input is _query, return must be a string).\n---\n\n# Python Code Node (Beta)\n\nExpert guidance for writing Python code in n8n Code nodes.\n\n---\n\n## ⚠️ Important: JavaScript First\n\n**Recommendation**: Use **JavaScript for 95% of use cases**. Only use Python when:\n- You need specific Python standard library functions\n- You're significantly more comfortable with Python syntax\n- You're doing data transformations better suited to Python\n\n**Why JavaScript is preferred:**\n- Full n8n helper functions (`this.helpers.httpRequest`, etc.)\n- Luxon DateTime library for advanced date/time operations\n- No external library limitations\n- Better n8n documentation and community support\n\n---\n\n## Quick Start\n\n```python\n# Basic template for Python Code nodes\nitems = _input.all()\n\n# Process data\nprocessed = []\nfor item in items:\n    processed.append({\n        \"json\": {\n            **item[\"json\"],\n            \"processed\": True,\n            \"timestamp\": datetime.now().isoformat()\n        }\n    })\n\nreturn processed\n```\n\n### Essential Rules\n\n1. **Consider JavaScript first** - Use Python only when necessary\n2. **Access data**: `_input.all()`, `_input.first()`, or `_input.item`\n3. **CRITICAL**: Must return `[{\"json\": {...}}]` format\n4. **CRITICAL**: Webhook data is under `_json[\"body\"]` (not `_json` directly)\n5. **CRITICAL LIMITATION**: **No external libraries** (no requests, pandas, numpy)\n6. **Standard library only**: json, datetime, re, base64, hashlib, urllib.parse, math, random, statistics\n\n---\n\n## Mode Selection Guide\n\nSame as JavaScript - choose based on your use case:\n\n### Run Once for All Items (Recommended - Default)\n\n**Use this mode for:** 95% of use cases\n\n- **How it works**: Code executes **once** regardless of input count\n- **Data access**: `_input.all()` or `_items` array (Native mode)\n- **Best for**: Aggregation, filtering, batch processing, transformations\n- **Performance**: Faster for multiple items (single execution)\n\n```python\n# Example: Calculate total from all items\nall_items = _input.all()\ntotal = sum(item[\"json\"].get(\"amount\", 0) for item in all_items)\n\nreturn [{\n    \"json\": {\n        \"total\": total,\n        \"count\": len(all_items),\n        \"average\": total / len(all_items) if all_items else 0\n    }\n}]\n```\n\n### Run Once for Each Item\n\n**Use this mode for:** Specialized cases only\n\n- **How it works**: Code executes **separately** for each input item\n- **Data access**: `_input.item` or `_item` (Native mode)\n- **Best for**: Item-specific logic, independent operations, per-item validation\n- **Performance**: Slower for large datasets (multiple executions)\n\n```python\n# Example: Add processing timestamp to each item\nitem = _input.item\n\nreturn [{\n    \"json\": {\n        **item[\"json\"],\n        \"processed\": True,\n        \"processed_at\": datetime.now().isoformat()\n    }\n}]\n```\n\n---\n\n## Python Modes: Beta vs Native\n\nn8n offers two Python execution modes:\n\n### Python (Beta) - Recommended\n- **Use**: `_input`, `_json`, `_node` helper syntax\n- **Best for**: Most Python use cases\n- **Helpers available**: `_now`, `_today`, `_jmespath()`\n- **Import**: `from datetime import datetime`\n\n```python\n# Python (Beta) example\nitems = _input.all()\nnow = _now  # Built-in datetime object\n\nreturn [{\n    \"json\": {\n        \"count\": len(items),\n        \"timestamp\": now.isoformat()\n    }\n}]\n```\n\n### Python (Native) (Beta)\n- **Use**: `_items`, `_item` variables only\n- **No helpers**: No `_input`, `_now`, etc.\n- **More limited**: Standard Python only\n- **Use when**: Need pure Python without n8n helpers\n\n```python\n# Python (Native) example\nprocessed = []\n\nfor item in _items:\n    processed.append({\n        \"json\": {\n            \"id\": item[\"json\"].get(\"id\"),\n            \"processed\": True\n        }\n    })\n\nreturn processed\n```\n\n**Recommendation**: Use **Python (Beta)** for better n8n integration.\n\n---\n\n## Data Access Patterns\n\nAccess input data through underscore-prefixed variables. Each item is a dict shaped `{\"json\": {...}}`, so the actual fields live under `[\"json\"]`.\n\n```python\n# Pattern 1: _input.all() - Most common. Arrays, batch ops, aggregations\nall_items = _input.all()            # list of {\"json\": {...}} dicts\n\n# Pattern 2: _input.first() - Very common. Single objects, API responses\ndata = _input.first()[\"json\"]       # built-in safety vs all_items[0]\n\n# Pattern 3: _input.item - \"Run Once for Each Item\" mode ONLY\ncurrent = _input.item[\"json\"]       # None/error in All Items mode\n\n# Pattern 4: _node - Reference a specific named node\nwebhook_data = _node[\"Webhook\"][\"json\"]\nhttp_data = _node[\"HTTP Request\"][\"json\"]\n```\n\n**See**: [DATA_ACCESS.md](DATA_ACCESS.md) for the comprehensive guide — six `_input.all()` recipes (filter, transform, aggregate, sort, group, deduplicate), `_input.first()` and `_input.item` examples, multi-node combining, the JS-vs-Python variable table, and the decision tree.\n\n---\n\n## Critical: Webhook Data Structure\n\n**MOST COMMON MISTAKE**: Webhook data is nested under `[\"body\"]`\n\n```python\n# ❌ WRONG - Will raise KeyError\nname = _json[\"name\"]\nemail = _json[\"email\"]\n\n# ✅ CORRECT - Webhook data is under [\"body\"]\nname = _json[\"body\"][\"name\"]\nemail = _json[\"body\"][\"email\"]\n\n# ✅ SAFER - Use .get() for safe access\nwebhook_data = _json.get(\"body\", {})\nname = webhook_data.get(\"name\")\n```\n\n**Why**: Webhook node wraps all request data under `body` property. This includes POST data, query parameters, and JSON payloads.\n\n**See**: [DATA_ACCESS.md](DATA_ACCESS.md) for full webhook structure details\n\n---\n\n## Return Format Requirements\n\n**CRITICAL RULE**: Always return list of dictionaries with `\"json\"` key\n\n### Correct Return Formats\n\n```python\n# ✅ Single result\nreturn [{\n    \"json\": {\n        \"field1\": value1,\n        \"field2\": value2\n    }\n}]\n\n# ✅ Multiple results\nreturn [\n    {\"json\": {\"id\": 1, \"data\": \"first\"}},\n    {\"json\": {\"id\": 2, \"data\": \"second\"}}\n]\n\n# ✅ List comprehension\ntransformed = [\n    {\"json\": {\"id\": item[\"json\"][\"id\"], \"processed\": True}}\n    for item in _input.all()\n    if item[\"json\"].get(\"valid\")\n]\nreturn transformed\n\n# ✅ Empty result (when no data to return)\nreturn []\n\n# ✅ Conditional return\nif should_process:\n    return [{\"json\": processed_data}]\nelse:\n    return []\n```\n\n### Incorrect Return Formats\n\n```python\n# ❌ WRONG: Dictionary without list wrapper\nreturn {\n    \"json\": {\"field\": value}\n}\n\n# ❌ WRONG: List without json wrapper\nreturn [{\"field\": value}]\n\n# ❌ WRONG: Plain string\nreturn \"processed\"\n\n# ❌ WRONG: Incomplete structure\nreturn [{\"data\": value}]  # Should be {\"json\": value}\n```\n\n**Why it matters**: Next nodes expect list format. Incorrect format causes workflow execution to fail.\n\n**See**: [ERROR_PATTERNS.md](ERROR_PATTERNS.md) #2 for detailed error solutions\n\n---\n\n## Critical Limitation: No External Libraries\n\n**MOST IMPORTANT PYTHON LIMITATION**: Cannot import external packages on default installs.\n\n> **Self-hosted exception**: external package availability depends entirely on the instance's Python runner configuration. If the user states their self-hosted instance has specific packages available in the Python runner environment, use them — don't refuse. When unsure, ask or write standard-library-only code.\n\n**❌ NOT available** (raise `ModuleNotFoundError`): `requests`, `pandas`, `numpy`, `scipy`, `bs4`/BeautifulSoup, `lxml`.\n\n**✅ Available** (standard library only): `json`, `datetime`, `re`, `base64`, `hashlib`, `urllib.parse`, `math`, `random`, `statistics`.\n\n### Workarounds\n\n**Need HTTP requests?**\n- ✅ Use **HTTP Request node** before Code node\n- ✅ Or switch to **JavaScript** and use `this.helpers.httpRequest()` (the bare `$helpers` global is undefined in the task-runner sandbox)\n\n**Need data analysis (pandas/numpy)?**\n- ✅ Use Python **statistics** module for basic stats\n- ✅ Or switch to **JavaScript** for most operations\n- ✅ Manual calculations with lists and dictionaries\n\n**Need web scraping (BeautifulSoup)?**\n- ✅ Use **HTTP Request node** + **HTML Extract node**\n- ✅ Or switch to **JavaScript** with regex/string methods\n\n**See**: [STANDARD_LIBRARY.md](STANDARD_LIBRARY.md) for complete reference\n\n---\n\n## Common Patterns Overview\n\nBased on production workflows, the most useful Python patterns are:\n\n1. **Data Transformation** - Transform all items with list comprehensions\n2. **Filtering & Aggregation** - Sum, filter, count with built-in functions\n3. **String Processing with Regex** - Extract patterns from text with `re`\n4. **Data Validation** - Validate and clean data, attach error lists\n5. **Statistical Analysis** - Calculate mean/median/stdev with the `statistics` module\n\nCopy-ready snippets for all five live in [COMMON_PATTERNS.md](COMMON_PATTERNS.md#quick-pattern-snippets), alongside 10 fully detailed production patterns (multi-source aggregation, markdown parsing, JSON comparison, CRM normalization, dictionary lookup, top-N filtering, and more).\n\n---\n\n## Error Prevention - Top 5 Mistakes\n\n1. **Importing external libraries** (Python-specific) → `import requests` raises `ModuleNotFoundError`. Use the HTTP Request node or JavaScript instead.\n2. **Empty code or missing return** → every path must end with `return [{\"json\": ...}]`.\n3. **Incorrect return format** → wrap in a list: `{\"json\": {...}}` becomes `[{\"json\": {...}}]`.\n4. **KeyError on dictionary access** → use `.get()`: `_json.get(\"user\", {}).get(\"name\", \"Unknown\")`.\n5. **Webhook body nesting** → read via `[\"body\"]`: `_json.get(\"body\", {}).get(\"email\", \"no-email\")`.\n\n**See**: [ERROR_PATTERNS.md](ERROR_PATTERNS.md) for the comprehensive guide — each error with wrong-vs-right code, error messages, nested-access fixes, an `AttributeError` bonus case, a prevention checklist, and a quick-fix table.\n\n---\n\n## Standard Library Reference\n\nMost useful modules: `json` (parse/generate), `datetime` (dates + `timedelta`), `re` (regex), `base64` (encode/decode), `hashlib` (hashing), `urllib.parse` (URL ops), and `statistics` (mean/median/stdev). Also available: `math`, `random`, `collections`, `itertools`, `functools`.\n\nFor a condensed cheat sheet plus full per-module examples, see [STANDARD_LIBRARY.md](STANDARD_LIBRARY.md#quick-reference-most-useful-modules).\n\n---\n\n## Best Practices\n\n### 1. Always Use .get() for Dictionary Access\n\n```python\n# ✅ SAFE: Won't crash if field missing\nvalue = item[\"json\"].get(\"field\", \"default\")\n\n# ❌ RISKY: Crashes if field doesn't exist\nvalue = item[\"json\"][\"field\"]\n```\n\n### 2. Handle None/Null Values Explicitly\n\n```python\n# ✅ GOOD: Default to 0 if None\namount = item[\"json\"].get(\"amount\") or 0\n\n# ✅ GOOD: Check for None explicitly\ntext = item[\"json\"].get(\"text\")\nif text is None:\n    text = \"\"\n```\n\n### 3. Use List Comprehensions for Filtering\n\n```python\n# ✅ PYTHONIC: List comprehension\nvalid = [item for item in items if item[\"json\"].get(\"active\")]\n\n# ❌ VERBOSE: Manual loop\nvalid = []\nfor item in items:\n    if item[\"json\"].get(\"active\"):\n        valid.append(item)\n```\n\n### 4. Return Consistent Structure\n\n```python\n# ✅ CONSISTENT: Always list with \"json\" key\nreturn [{\"json\": result}]  # Single result\nreturn results  # Multiple results (already formatted)\nreturn []  # No results\n```\n\n### 5. Debug with print() Statements\n\n```python\n# Debug statements appear in browser console (F12)\nitems = _input.all()\nprint(f\"Processing {len(items)} items\")\nprint(f\"First item: {items[0] if items else 'None'}\")\n```\n\n---\n\n## Production Gotchas\n\n### SplitInBatches Loop Semantics\n\nThe SplitInBatches node has two outputs:\n- `main[0]` = **done** — fires ONCE after all batches complete\n- `main[1]` = **each batch** — fires for every batch (the loop body)\n\nAlways add a **Limit 1** node after the done output.\n\n### Correct Node Reference Syntax\n\n```python\n# ❌ WRONG\ndata = _node['HTTP Request']['json']\n\n# ✅ CORRECT - call .first() then access json\ndata = _node['HTTP Request'].first()['json']\n```\n\n### Cross-Iteration Data Not Available in Python\n\n`$getWorkflowStaticData('global')` may not be available in Python Beta mode. If you need to accumulate data across SplitInBatches iterations, use a JavaScript Code node for the accumulation logic instead.\n\n---\n\n## When to Use Python vs JavaScript\n\n### Use Python When:\n- ✅ You need `statistics` module for statistical operations\n- ✅ You're significantly more comfortable with Python syntax\n- ✅ Your logic maps well to list comprehensions\n- ✅ You need specific standard library functions\n\n### Use JavaScript When:\n- ✅ You need HTTP requests (`this.helpers.httpRequest()`)\n- ✅ You need advanced date/time (DateTime/Luxon)\n- ✅ You want better n8n integration\n- ✅ **For 95% of use cases** (recommended)\n\n### Consider Other Nodes When:\n- ❌ Simple field mapping → Use **Set** node\n- ❌ Basic filtering → Use **Filter** node\n- ❌ Simple conditionals → Use **IF** or **Switch** node\n- ❌ HTTP requests only → Use **HTTP Request** node\n\n---\n\n## Integration with Other Skills\n\n### Works With:\n\n**n8n Expression Syntax**:\n- Expressions use `{{ }}` syntax in other nodes\n- Code nodes use Python directly (no `{{ }}`)\n- When to use expressions vs code\n\n**n8n MCP Tools Expert**:\n- How to find Code node: `search_nodes({query: \"code\"})`\n- Get configuration help: `get_node({nodeType: \"nodes-base.code\"})`\n- Validate code: `validate_node({nodeType: \"nodes-base.code\", config: {...}})`\n\n**n8n Node Configuration**:\n- Mode selection (All Items vs Each Item)\n- Language selection (Python vs JavaScript)\n- Understanding property dependencies\n\n**n8n Workflow Patterns**:\n- Code nodes in transformation step\n- When to use Python vs JavaScript in patterns\n\n**n8n Validation Expert**:\n- Validate Code node configuration\n- Handle validation errors\n- Auto-fix common issues\n\n**n8n Code JavaScript**:\n- When to use JavaScript instead\n- Comparison of JavaScript vs Python features\n- Migration from Python to JavaScript\n\n---\n\n## Quick Reference Checklist\n\nBefore deploying Python Code nodes, verify:\n\n- [ ] **Considered JavaScript first** - Using Python only when necessary\n- [ ] **Code is not empty** - Must have meaningful logic\n- [ ] **Return statement exists** - Must return list of dictionaries\n- [ ] **Proper return format** - Each item: `{\"json\": {...}}`\n- [ ] **Data access correct** - Using `_input.all()`, `_input.first()`, or `_input.item`\n- [ ] **No external imports** - Only standard library (json, datetime, re, etc.)\n- [ ] **Safe dictionary access** - Using `.get()` to avoid KeyError\n- [ ] **Webhook data** - Access via `[\"body\"]` if from webhook\n- [ ] **Mode selection** - \"All Items\" for most cases\n- [ ] **Output consistent** - All code paths return same structure\n\n---\n\n## Additional Resources\n\n### Related Files\n- [DATA_ACCESS.md](DATA_ACCESS.md) - Comprehensive Python data access patterns\n- [COMMON_PATTERNS.md](COMMON_PATTERNS.md) - 10 Python patterns for n8n\n- [ERROR_PATTERNS.md](ERROR_PATTERNS.md) - Top 5 errors and solutions\n- [STANDARD_LIBRARY.md](STANDARD_LIBRARY.md) - Complete standard library reference\n\n### n8n Documentation\n- Code Node Guide: https://docs.n8n.io/code/code-node/\n- Python in n8n: https://docs.n8n.io/code/builtin/python-modules/\n\n---\n\n**Ready to write Python in n8n Code nodes - but consider JavaScript first!** Use Python for specific needs, reference the error patterns guide to avoid common mistakes, and leverage the standard library effectively.\n","data/skills/n8n-code-javascript/SKILL.md":"---\nname: n8n-code-javascript\ndescription: Write JavaScript code in n8n Code nodes. Use when writing JavaScript in n8n, using $input/$json/$node syntax, making HTTP requests with this.helpers / the $helpers global, working with dates using DateTime, troubleshooting Code node errors, choosing between Code node modes, or doing any custom data transformation in n8n. Always use this skill when a workflow needs a Code node — whether for data aggregation, filtering, API calls, format conversion, batch processing logic, or any custom JavaScript. Covers SplitInBatches loop patterns, cross-iteration data, pairedItem, and real-world production patterns. Also use when asked why a Code node or workflow is slow, which execution mode is faster, or how to cut per-item overhead on large datasets. EXCEPTION — for the AI-agent-callable Custom Code Tool (@n8n/n8n-nodes-langchain.toolCode, a tool attached to an AI Agent), use the n8n-code-tool skill instead; it has a different runtime contract.\n---\n\n# JavaScript Code Node\n\nExpert guidance for writing JavaScript code in n8n Code nodes.\n\n---\n\n## Quick Start\n\n```javascript\n// Basic template for Code nodes\nconst items = $input.all();\n\n// Process data\nconst processed = items.map(item => ({\n  json: {\n    ...item.json,\n    processed: true,\n    timestamp: new Date().toISOString()\n  }\n}));\n\nreturn processed;\n```\n\n### Essential Rules\n\n1. **Choose \"Run Once for All Items\" mode** (recommended for most use cases)\n2. **Access data**: `$input.all()`, `$input.first()`, or `$input.item`\n3. **Return `[{json: {...}}]`** — the canonical, mode-portable form. In *Run Once for All Items* mode n8n also auto-wraps a bare `return {…}` object, so that runs too; what genuinely fails is returning a primitive (string/number) or `null`.\n4. **CRITICAL**: Webhook data is under `$json.body` (not `$json` directly)\n5. **Built-ins available**: `this.helpers.httpRequest()` (no auth — the bare `$helpers` global is **undefined** in the task-runner sandbox, so `$helpers.httpRequest()` throws `ReferenceError: $helpers is not defined`), DateTime (Luxon), $jmespath(). **Not available**: `this.helpers.httpRequestWithAuthentication` (deny-listed), $env (when N8N_BLOCK_ENV_ACCESS_IN_NODE=true), require() (unless allowlisted). For anything beyond a trivial unauthenticated GET (auth, pagination, retries), prefer the **HTTP Request node** and keep Code nodes for pure logic.\n6. **Instance-allowlisted libraries**: Self-hosted instances can allowlist modules via `N8N_RUNNERS_ALLOWED_BUILT_IN_MODULES` and `N8N_RUNNERS_ALLOWED_EXTERNAL_MODULES` (legacy: `NODE_FUNCTION_ALLOW_BUILTIN` / `NODE_FUNCTION_ALLOW_EXTERNAL`). If the user says their instance allows specific modules (e.g. `axios`, `lodash`, `crypto`), use them via `require()` — don't refuse. If unsure, ask or default to built-ins only.\n7. **Wrong skill?** If you're writing code for a **Custom Code Tool** attached to an AI Agent (`@n8n/n8n-nodes-langchain.toolCode`), stop — that node has a different contract (input via `query`, must return a string, no `$input`/`$helpers`). Use the **n8n-code-tool** skill.\n\n---\n\n## Mode Selection Guide\n\nThe Code node offers two execution modes. Choose based on your use case:\n\n### Run Once for All Items (Recommended - Default)\n\n**Use this mode for:** 95% of use cases\n\n- **How it works**: Code executes **once** regardless of input count\n- **Data access**: `$input.all()` or `items` array\n- **Best for**: Aggregation, filtering, batch processing, transformations, API calls with all data\n- **Performance**: Faster for multiple items (single execution)\n\n```javascript\n// Example: Calculate total from all items\nconst allItems = $input.all();\nconst total = allItems.reduce((sum, item) => sum + (item.json.amount || 0), 0);\n\nreturn [{\n  json: {\n    total,\n    count: allItems.length,\n    average: total / allItems.length\n  }\n}];\n```\n\n**When to use:**\n- ✅ Comparing items across the dataset\n- ✅ Calculating totals, averages, or statistics\n- ✅ Sorting or ranking items\n- ✅ Deduplication\n- ✅ Building aggregated reports\n- ✅ Combining data from multiple items\n\n### Run Once for Each Item\n\n**Use this mode for:** Specialized cases only\n\n- **How it works**: Code executes **separately** for each input item\n- **Data access**: `$input.item` or `$item`\n- **Best for**: Item-specific logic, independent operations, per-item validation\n- **Performance**: Slower for large datasets (multiple executions)\n\n```javascript\n// Example: Add processing timestamp to each item\nconst item = $input.item;\n\nreturn [{\n  json: {\n    ...item.json,\n    processed: true,\n    processedAt: new Date().toISOString()\n  }\n}];\n```\n\n**When to use:**\n- ✅ Each item needs independent API call\n- ✅ Per-item validation with different error handling\n- ✅ Item-specific transformations based on item properties\n- ✅ When items must be processed separately for business logic\n\n**Decision Shortcut:**\n- **Need to look at multiple items?** → Use \"All Items\" mode\n- **Each item completely independent?** → Use \"Each Item\" mode\n- **Not sure?** → Use \"All Items\" mode (you can always loop inside)\n\n### Why \"All Items\" is faster — the per-item boundary\n\nMode choice is the single biggest performance lever in a Code node. Each *per-item* execution context costs a setup tax (measured on n8n 2.x, small records):\n\n| What runs per item | Approx. cost |\n|---|---|\n| Code **All Items** (one run for the whole set) | ~0.02 ms/item |\n| Expression in any node (IF / Set / etc.) | ~0.2 ms/item |\n| Code **Each Item** (a full sandbox per item) | ~0.6 ms/item — ~25–30× All Items |\n\nSo `Run Once for Each Item` over 10k items is ~6 s of pure overhead vs ~0.2 s in `Run Once for All Items`. Use Each Item only when an item genuinely needs isolating (independent error handling, or a per-item API call you can't batch); otherwise loop *inside* one All Items node. Expression complexity itself is essentially free (~90% of the cost is the per-item context, not your code) and every node→node hop re-copies all items — so reduce the *number* of per-item boundaries, don't micro-optimize each one. Below a few hundred items none of this matters; reach for it on the hot path (large item counts, little I/O).\n\n**See**: [DATA_ACCESS.md](DATA_ACCESS.md) → \"Mode Performance\" for the corollaries, hop costs, and scale check.\n\n---\n\n## Data Access Patterns\n\nFour ways to pull data from upstream nodes. Note `$node[\"Name\"]` and `$('Name')` need `.first().json` or `.all()` — never `.json` directly.\n\n```javascript\nconst allItems = $input.all();          // 1. All items — batch ops, aggregation (most common)\nconst data = $input.first().json;       // 2. First item — single objects, API responses\nconst item = $input.item;               // 3. Current item — \"Each Item\" mode ONLY (undefined otherwise)\nconst other = $node[\"Webhook\"].json;    // 4. Named node — combine data across nodes\n```\n\nAlways access fields via `.json` (e.g. `item.json.name`, not `item.name`), and prefer the explicit `$input.first().json.field` over a bare `$json.field`.\n\n**See**: [DATA_ACCESS.md](DATA_ACCESS.md) for the full guide — every pattern with examples, a decision tree, and the common mistakes (mutating originals, missing length checks, `$input.item` in the wrong mode).\n\n---\n\n## Critical: Webhook Data Structure\n\n**MOST COMMON MISTAKE**: Webhook data is nested under `.body`\n\n```javascript\n// ❌ WRONG - Will return undefined\nconst name = $json.name;\nconst email = $json.email;\n\n// ✅ CORRECT - Webhook data is under .body\nconst name = $json.body.name;\nconst email = $json.body.email;\n\n// Or with $input\nconst webhookData = $input.first().json.body;\nconst name = webhookData.name;\n```\n\n**Why**: Webhook node wraps all request data under `body` property. This includes POST data, query parameters, and JSON payloads.\n\n**See**: [DATA_ACCESS.md](DATA_ACCESS.md) for full webhook structure details\n\n---\n\n## Return Format Requirements\n\n**Canonical form**: `[{json: {...}}]` — an array of objects each with a `json` property. It is unambiguous and works identically in both execution modes, so make it your default.\n\nIn *Run Once for All Items* mode n8n auto-normalizes looser shapes on the way out: a single bare object, or an array of bare objects, gets wrapped under `json` for you. So `return {foo: 1}` runs. What has nothing to wrap — and therefore genuinely fails at runtime with \"Code doesn't return items properly\" — is a primitive (string/number/boolean) or `null`/`undefined`. (n8n-mcp ≥ 2.63.0 no longer flags a bare-object return as an error; it reflects this auto-wrap behavior.)\n\n### Correct Return Formats\n\n```javascript\n// ✅ Single result\nreturn [{\n  json: {\n    field1: value1,\n    field2: value2\n  }\n}];\n\n// ✅ Multiple results\nreturn [\n  {json: {id: 1, data: 'first'}},\n  {json: {id: 2, data: 'second'}}\n];\n\n// ✅ Transformed array\nconst transformed = $input.all()\n  .filter(item => item.json.valid)\n  .map(item => ({\n    json: {\n      id: item.json.id,\n      processed: true\n    }\n  }));\nreturn transformed;\n\n// ✅ Empty result (when no data to return)\nreturn [];\n\n// ✅ Conditional return\nif (shouldProcess) {\n  return [{json: processedData}];\n} else {\n  return [];\n}\n```\n\n### Non-Canonical Returns (auto-wrapped — prefer the canonical form)\n\n```javascript\n// ⚠️ Auto-wrapped in All Items mode → [{json: {field: value}}]. Runs, but prefer the array form.\nreturn {\n  json: {field: value}\n};\n\n// ⚠️ Auto-wrapped → [{json: {field: value}}]. Runs, but add the json wrapper for clarity.\nreturn [{field: value}];\n\n// ✅ Fine — input items already carry a json property, so returning them unchanged is a valid passthrough\nreturn $input.all();\n```\n\n### Genuinely Broken Returns\n\n```javascript\n// ❌ FAILS: primitive — n8n errors \"Code doesn't return items properly\"\nreturn \"processed\";\n\n// ❌ FAILS: null / undefined — nothing to pass to the next node\nreturn null;\n```\n\n**Why it matters**: The canonical `[{json: {...}}]` is unambiguous and behaves the same in both modes. n8n auto-normalizes bare objects and arrays-of-objects in All Items mode, but a primitive or `null` return has nothing to wrap and stops execution.\n\n**See**: [ERROR_PATTERNS.md](ERROR_PATTERNS.md) #3 for detailed error solutions\n\n---\n\n## Common Patterns Overview\n\nThe most useful Code node shapes from production workflows. One quick example — sum/aggregate across all items:\n\n```javascript\nconst items = $input.all();\nconst total = items.reduce((sum, item) => sum + (item.json.amount || 0), 0);\nreturn [{ json: { total, count: items.length, average: total / items.length } }];\n```\n\nThe full library covers 10 patterns: multi-source aggregation, regex filtering, markdown/structured-text parsing, JSON comparison, CRM/form transformation, release processing, array transformation with computed fields, Slack Block Kit formatting, top-N ranking, and string-aggregation reporting — each with variations.\n\n**See**: [COMMON_PATTERNS.md](COMMON_PATTERNS.md) for the 10 detailed production patterns (and the Best Practices section: validate input, try-catch, filter-early, array methods over loops, console.log debugging).\n\n---\n\n## Error Prevention - Top Mistakes\n\nThe recurring Code node failures, in rough frequency order:\n\n1. **Empty code / missing return** — always end with `return [...]`, and make sure *every* branch returns.\n2. **Expression syntax as code** — don't write `{{ }}` where JavaScript belongs (`return {{ $json.x }}` is a syntax error). Use `` `${$json.field}` `` or `$input.first().json.field`. `{{ }}` *inside a string literal* is fine — it's just literal text n8n won't evaluate.\n3. **Return shape** — prefer `return [{json:{...}}]`. A bare `return {…}` auto-wraps in All Items mode, but returning a primitive (string/number) or `null` is what actually fails.\n4. **Missing null checks** — use optional chaining: `item.json?.user?.email || 'fallback'`.\n5. **Webhook body nesting** — `$json.email` is undefined; use `$json.body.email`.\n6. **Auth helpers blocked** (`httpRequestWithAuthentication`) and `$env` blocked — route secrets through credentials/HTTP Request node, not the Code node sandbox.\n\n**See**: [ERROR_PATTERNS.md](ERROR_PATTERNS.md) for the comprehensive guide — each error with wrong/right code, escaping rules, the sandbox restrictions (Errors #6–#7), a prevention checklist, and a quick error-message lookup table.\n\n---\n\n## Built-in Functions & Helpers\n\n```javascript\n// HTTP requests (no auth — see sandbox note below)\nconst res = await this.helpers.httpRequest({ method: 'GET', url: 'https://api.example.com/data' });\n\n// DateTime (Luxon): now, formatting, arithmetic\nconst now = DateTime.now();\nconst formatted = now.toFormat('yyyy-MM-dd');\nconst tomorrow = now.plus({ days: 1 });\n\n// $jmespath() — query JSON structures\nconst adults = $jmespath($input.first().json, 'users[?age >= `18`]');\n\n// $getWorkflowStaticData() — data that persists across executions\n```\n\n**Sandbox (since n8n v2.0, JsTaskRunnerSandbox):** the accessor is `this.helpers.httpRequest()` — the bare `$helpers` global is **undefined** here (`$helpers.httpRequest()` throws `ReferenceError`). Inside a nested async function where `this` is lost, call it as `await fn.call(this, ...)`. `this.helpers.httpRequestWithAuthentication` and `this.helpers.requestWithAuthenticationPaginated` are deny-listed (→ `UnsupportedFunctionError`); for authenticated calls use an **HTTP Request node** with the credential (preferred), a sub-workflow, or a manual `Authorization: Bearer ${token}` header on `this.helpers.httpRequest()` only when the token already flows through the workflow as data. `$env` is blocked when `N8N_BLOCK_ENV_ACCESS_IN_NODE=true`; `require()` works only for allowlisted modules. `Buffer`, `URL`, and standard JS globals (Math, JSON, Object, Array) always work.\n\n**See**: [BUILTIN_FUNCTIONS.md](BUILTIN_FUNCTIONS.md) for the complete reference — full httpRequest options, all DateTime/Luxon operations, JMESPath patterns, static-data use cases, and the sandbox-restriction details.\n\n---\n\n## Best Practices\n\n- **Validate input first** — guard for empty arrays / missing `.json` before processing.\n- **Try-catch risky work** (HTTP calls) and return an error object instead of crashing.\n- **Prefer array methods** (`filter`/`map`/`reduce`) over manual loops.\n- **Filter early, transform late** — shrink the dataset before expensive work.\n- **Descriptive names** and `console.log()` for debugging (output goes to the browser console).\n\n**See**: [COMMON_PATTERNS.md](COMMON_PATTERNS.md) → \"Best Practices\" for code examples of each.\n\n---\n\n## Production Gotchas\n\nHard-won lessons from real deployments — summarized here, with code in [DATA_ACCESS.md](DATA_ACCESS.md) → \"Production Gotchas\":\n\n- **SplitInBatches outputs are counterintuitive**: `main[0]` = **done** (fires once, after all batches), `main[1]` = **each batch** (the loop body). Add a **Limit 1** node after the done output as a safety.\n- **Iteration count is the cost**: each loop iteration re-runs the whole body through the engine (~0.8 ms overhead each). `batchSize: 1` is the loop equivalent of *Each Item* — use the largest batch your real constraint (rate limit, page size, memory) allows, or don't loop at all.\n- **Cross-iteration accumulation (CRITICAL)**: after the loop, `$('Node Inside Loop').all()` returns ONLY the last iteration's items. Accumulate via `$getWorkflowStaticData('global')` (reset before, push inside, read after).\n- **pairedItem**: when emitting items that don't map 1:1 to input, set `pairedItem: { item: i }` or downstream Set nodes fail with `paired_item_no_info`.\n- **Node reference syntax**: `$('Node').first().json` or `$('Node').all()` — never `.json` directly on the reference.\n- **Float precision**: compare currency at the cent level — `Math.round(a*100) !== Math.round(b*100)` — to avoid false positives from float noise.\n\n---\n\n## When to Use Code Node\n\n> **Before reaching for a Code node, walk the transform gatekeeper** in the n8n Expression Syntax skill: expression → arrow-function IIFE inside an Edit Fields field → Code node, in that order. The first two paths cover most \"transform this data\" tasks at ~1–10ms each, versus the Code node's sandboxed ~500–1000ms — a ~100x gap on pure single-item shaping, with no functional difference. The Code node earns its place only for whole-dataset aggregation (`$input.all()`), allowlisted libraries, or async work. And before writing code for crypto (HMAC, hashing, signing) or XML/SOAP/RSS parsing, check for a **native node** — n8n has a Crypto node (`nodes-base.crypto`) and an XML node (`nodes-base.xml`) that cover those without any JavaScript. Dropping into Code for something a native node already does is one of the most common false positives.\n\nUse Code node when:\n- ✅ Complex transformations requiring multiple steps\n- ✅ Custom calculations or business logic\n- ✅ Recursive operations\n- ✅ API response parsing with complex structure\n- ✅ Multi-step conditionals\n- ✅ Data aggregation across items\n\nConsider other nodes when:\n- ❌ Simple field mapping → Use **Set** node\n- ❌ Basic filtering → Use **Filter** node\n- ❌ Simple conditionals → Use **IF** or **Switch** node\n- ❌ HTTP requests only → Use **HTTP Request** node\n\n**Code node excels at**: Complex logic that would require chaining many simple nodes\n\n---\n\n## Integration with Other Skills\n\n### Works With:\n\n**n8n Expression Syntax**:\n- Expressions use `{{ }}` syntax in other nodes\n- Code nodes use JavaScript directly (no `{{ }}`)\n- When to use expressions vs code\n\n**n8n MCP Tools Expert**:\n- How to find Code node: `search_nodes({query: \"code\"})`\n- Get configuration help: `get_node({nodeType: \"nodes-base.code\"})`\n- Validate code: `validate_node({nodeType: \"nodes-base.code\", config: {...}})`\n\n**n8n Node Configuration**:\n- Mode selection (All Items vs Each Item)\n- Language selection (JavaScript vs Python)\n- Understanding property dependencies\n\n**n8n Workflow Patterns**:\n- Code nodes in transformation step\n- Webhook → Code → API pattern\n- Error handling in workflows\n\n**n8n Validation Expert**:\n- Validate Code node configuration\n- Handle validation errors\n- Auto-fix common issues\n\n---\n\n## Quick Reference Checklist\n\nBefore deploying Code nodes, verify:\n\n- [ ] **Code is not empty** - Must have meaningful logic\n- [ ] **Return statement exists** - Returns items, not a primitive/`null`\n- [ ] **Canonical return format** - Each item: `{json: {...}}` (bare objects auto-wrap, but be explicit)\n- [ ] **Data access correct** - Using `$input.all()`, `$input.first()`, or `$input.item`\n- [ ] **No `{{ }}` written as code** - Use JavaScript template literals: `` `${value}` ``\n- [ ] **Error handling** - Guard clauses for null/undefined inputs\n- [ ] **Webhook data** - Access via `.body` if from webhook\n- [ ] **Mode selection** - \"All Items\" for most cases\n- [ ] **Performance** - Prefer map/filter over manual loops\n- [ ] **Output consistent** - All code paths return same structure\n\n---\n\n## Additional Resources\n\n### Related Files\n- [DATA_ACCESS.md](DATA_ACCESS.md) - Comprehensive data access patterns\n- [COMMON_PATTERNS.md](COMMON_PATTERNS.md) - 10 production-tested patterns\n- [ERROR_PATTERNS.md](ERROR_PATTERNS.md) - Top 5 errors and solutions\n- [BUILTIN_FUNCTIONS.md](BUILTIN_FUNCTIONS.md) - Complete built-in reference\n\n### n8n Documentation\n- Code Node Guide: https://docs.n8n.io/code/code-node/\n- Built-in Methods: https://docs.n8n.io/code-examples/methods-variables-reference/\n- Luxon Documentation: https://moment.github.io/luxon/\n\n---\n\n**Ready to write JavaScript in n8n Code nodes!** Start with simple transformations, use the error patterns guide to avoid common mistakes, and reference the pattern library for production-ready examples.\n","data/skills/n8n-mcp-tools-expert/SKILL.md":"---\nname: n8n-mcp-tools-expert\ndescription: Expert guide for using n8n-mcp MCP tools effectively. Use when searching for nodes, validating configurations, accessing templates, managing workflows, organizing workflows into folders, managing credentials, auditing instance security, or using any n8n-mcp tool. Provides tool selection guidance, parameter formats, and common patterns. IMPORTANT — Always consult this skill before calling any n8n-mcp tool — it prevents common mistakes like wrong nodeType formats, incorrect parameter structures, and inefficient tool usage. If the user mentions n8n, workflows, nodes, or automation and you have n8n MCP tools available, use this skill first.\n---\n\n# n8n MCP Tools Expert\n\nMaster guide for using n8n-mcp MCP server tools to build workflows.\n\n---\n\n## Tool Categories\n\nn8n-mcp provides tools organized into categories:\n\n1. **Node Discovery** → [SEARCH_GUIDE.md](SEARCH_GUIDE.md)\n2. **Configuration Validation** → [VALIDATION_GUIDE.md](VALIDATION_GUIDE.md)\n3. **Workflow Management** → [WORKFLOW_GUIDE.md](WORKFLOW_GUIDE.md)\n4. **Template Library** - Search and deploy 2,700+ real workflows\n5. **Data Tables** - Manage n8n data tables, rows and columns (`n8n_manage_datatable`)\n6. **Workflow Folders** - Folder CRUD + workflow placement (`n8n_manage_folders`)\n7. **Credential Management** - Full credential CRUD + schema discovery (`n8n_manage_credentials`)\n8. **Security & Audit** - Instance security auditing with custom deep scan (`n8n_audit_instance`)\n9. **Documentation & Guides** - Tool docs, AI agent guide, Code node guides\n10. **Agents** - Create, configure, validate, run and publish persisted n8n Agents (`n8n_manage_agents`, requires `N8N_MCP_ACCESS_TOKEN`)\n11. **Node Resource Resolution** - Resolve live dropdown/resource-locator values with a real credential (`n8n_explore_node_resources`, requires `N8N_MCP_ACCESS_TOKEN`)\n12. **Instance Catalog** - List projects and tags (`n8n_list_catalog`)\n\n---\n\n## Quick Reference\n\n### Most Used Tools (by success rate)\n\n| Tool | Use When | Speed |\n|------|----------|-------|\n| `search_nodes` | Finding nodes by keyword | <20ms |\n| `get_node` | Understanding node operations (detail=\"standard\") | <10ms |\n| `validate_node` | Checking configurations (mode=\"full\") | <100ms |\n| `n8n_create_workflow` | Creating workflows | 100-500ms |\n| `n8n_update_partial_workflow` | Editing workflows (MOST USED!) | 50-200ms |\n| `validate_workflow` | Checking complete workflow | 100-500ms |\n| `n8n_deploy_template` | Deploy template to n8n instance | 200-500ms |\n| `n8n_manage_datatable` | Managing data tables and rows | 50-500ms |\n| `n8n_manage_folders` | Folder CRUD + organizing workflows | 100-500ms |\n| `n8n_manage_credentials` | Credential CRUD + schema discovery | 50-500ms |\n| `n8n_audit_instance` | Security audit (built-in + custom scan) | 500-5000ms |\n| `n8n_autofix_workflow` | Auto-fix validation errors | 200-1500ms |\n| `n8n_manage_agents` | Persisted n8n Agent CRUD/validate/publish | 150-400ms; `call` action: 5-60s |\n| `n8n_explore_node_resources` | Resolve live loadOptions/listSearch values | 200 ms - 5 s |\n| `n8n_list_catalog` | List projects or tags | 50-300ms |\n\n---\n\n## Tool Selection Guide\n\n### Finding the Right Node\n\n**Workflow**:\n```\n1. search_nodes({query: \"keyword\"})\n2. get_node({nodeType: \"nodes-base.name\"})\n3. [Optional] get_node({nodeType: \"nodes-base.name\", mode: \"docs\"})\n```\n\n**Example**:\n```javascript\n// Step 1: Search\nsearch_nodes({query: \"slack\"})\n// Returns: nodes-base.slack\n\n// Step 2: Get details\nget_node({nodeType: \"nodes-base.slack\"})\n// Returns: operations, properties, examples (standard detail)\n\n// Step 3: Get readable documentation\nget_node({nodeType: \"nodes-base.slack\", mode: \"docs\"})\n// Returns: markdown documentation\n```\n\n**Common pattern**: search → get_node (18s average)\n\n### Validating Configuration\n\n**Workflow**:\n```\n1. validate_node({nodeType, config: {}, mode: \"minimal\"}) - Check required fields\n2. validate_node({nodeType, config, profile: \"runtime\"}) - Full validation\n3. [Repeat] Fix errors, validate again\n```\n\n**Common pattern**: validate → fix → validate (23s thinking, 58s fixing per cycle)\n\n### Managing Workflows\n\n**Workflow**:\n```\n1. n8n_create_workflow({name, nodes, connections})\n2. n8n_validate_workflow({id})\n3. n8n_update_partial_workflow({id, operations: [...]})\n4. n8n_validate_workflow({id}) again\n5. n8n_update_partial_workflow({id, operations: [{type: \"activateWorkflow\"}]})\n```\n\n**Common pattern**: iterative updates (56s average between edits)\n\n### Critical: Node JSON Hygiene When Creating Workflows\n\nThree structural mistakes in generated node JSON break the n8n UI even when the workflow validates:\n\n1. **Never emit a `credentials` block with a placeholder ID.** A fake ID like `\"id\": \"REPLACE_ME\"` renders the credential selector permanently disabled and non-clickable in the n8n UI (\"No credentials yet\") — the user has to recreate the node from scratch. If you don't know the real credential ID, **omit the `credentials` block entirely**; an absent block shows a normal empty dropdown the user can click. Use `n8n_manage_credentials({action: \"list\"})` to discover real credential IDs first.\n\n```javascript\n// ❌ Breaks the credential selector\n\"credentials\": {\"httpHeaderAuth\": {\"id\": \"REPLACE_ME\", \"name\": \"My API Key\"}}\n\n// ✅ Unknown ID → omit credentials block; user picks in UI\n// ✅ Known ID (from n8n_manage_credentials list) → use the real ID\n```\n\n2. **Generate UUID v4 values for node `id`** — not human-readable strings like `\"http-list-node\"`. n8n's frontend uses node IDs for form binding and credential component initialization; non-UUID IDs cause subtle UI breakage.\n\n3. **Use the current `typeVersion`** for each node — check `get_node` rather than hardcoding remembered versions (e.g. httpRequest is at 4.4+, not 4.2).\n\n---\n\n## Critical: nodeType Formats\n\n**Two different formats** for different tools!\n\n### Format 1: Search/Validate Tools\n```javascript\n// Use SHORT prefix\n\"nodes-base.slack\"\n\"nodes-base.httpRequest\"\n\"nodes-base.webhook\"\n\"nodes-langchain.agent\"\n```\n\n**Tools that use this**:\n- search_nodes (returns this format)\n- get_node\n- validate_node\n- validate_workflow\n\n### Format 2: Workflow Tools\n```javascript\n// Use FULL prefix\n\"n8n-nodes-base.slack\"\n\"n8n-nodes-base.httpRequest\"\n\"n8n-nodes-base.webhook\"\n\"@n8n/n8n-nodes-langchain.agent\"\n```\n\n**Tools that use this**:\n- n8n_create_workflow\n- n8n_update_partial_workflow\n\n### Conversion\n\n```javascript\n// search_nodes returns BOTH formats\n{\n  \"nodeType\": \"nodes-base.slack\",          // For search/validate tools\n  \"workflowNodeType\": \"n8n-nodes-base.slack\"  // For workflow tools\n}\n```\n\n---\n\n## Common Mistakes\n\nEight recurring mistakes. Two are worth showing in full because they silently corrupt structure:\n\n```javascript\n// nodeType prefix (search/validate tools want the SHORT form)\nget_node({nodeType: \"slack\"})              // ❌ missing prefix → \"Node not found\"\nget_node({nodeType: \"n8n-nodes-base.slack\"}) // ❌ FULL prefix is for workflow tools\nget_node({nodeType: \"nodes-base.slack\"})     // ✅\n\n// credentials must be nested by type with {id, name} — not a flat string\nupdates: {credentials: \"myApiKey\"}                              // ❌\nupdates: {credentials: {httpHeaderAuth: {id: \"abc123\", name: \"My API Key\"}}}  // ✅\n```\n\n| # | Mistake | Fix |\n|---|---------|-----|\n| 1 | Wrong nodeType format | SHORT `nodes-base.*` for search/validate; FULL `n8n-nodes-base.*` for workflow tools (see above) |\n| 2 | `detail: \"full\"` by default | Default `standard` covers 95%; reach for `docs`/`search_properties` instead of `full` |\n| 3 | No validation profile | Pass `profile: \"runtime\"` explicitly (`minimal`/`ai-friendly`/`strict` for other stages) |\n| 4 | Ignoring auto-sanitization | ALL nodes sanitized on ANY update (operator structures, IF/Switch metadata); it can't fix broken connections or branch-count mismatches |\n| 5 | Not using smart parameters | Use `branch: \"true\"` / `case: 0` instead of fragile `sourceIndex` math |\n| 6 | Omitting `intent` | Always include `intent` on `n8n_update_partial_workflow` for better responses |\n| 7 | `parameters` instead of `updates` | `updateNode` takes `updates: {...}`, not `parameters: {...}` |\n| 8 | Wrong credential format | Nest by type with `{id, name}` (see above) |\n\nFull WRONG/CORRECT examples for each: see [VALIDATION_GUIDE.md → Common Mistakes](VALIDATION_GUIDE.md).\n\n---\n\n## Tool Usage Patterns\n\nThree patterns dominate real usage. Worked, step-by-step examples for each live in the reference guides.\n\n- **Pattern 1 — Node Discovery** (18s avg between steps): `search_nodes({query})` → `get_node({nodeType, includeExamples: true})`. See [SEARCH_GUIDE.md](SEARCH_GUIDE.md).\n- **Pattern 2 — Validation Loop** (23s thinking, 58s fixing): `validate_node({profile: \"runtime\"})` → read `errors` → fix config → validate again until clean. See [VALIDATION_GUIDE.md](VALIDATION_GUIDE.md).\n- **Pattern 3 — Workflow Editing** (99.0% success, 56s avg between edits): iterate `n8n_update_partial_workflow` (with `intent`) → `n8n_validate_workflow` → finally `activateWorkflow`. Build iteratively, NOT one-shot. See [WORKFLOW_GUIDE.md](WORKFLOW_GUIDE.md).\n\n---\n\n## Detailed Guides\n\n### Node Discovery Tools\nSee [SEARCH_GUIDE.md](SEARCH_GUIDE.md) for:\n- search_nodes\n- get_node with detail levels (minimal, standard, full)\n- get_node modes (info, docs, search_properties, versions)\n\n### Validation Tools\nSee [VALIDATION_GUIDE.md](VALIDATION_GUIDE.md) for:\n- Validation profiles explained\n- validate_node with modes (minimal, full)\n- validate_workflow complete structure\n- Auto-sanitization system\n- Handling validation errors\n\n### Workflow Management\nSee [WORKFLOW_GUIDE.md](WORKFLOW_GUIDE.md) for:\n- n8n_create_workflow\n- n8n_update_partial_workflow (21 operation types including patchNodeField, setNodeGroups, and moveToFolder!)\n- Smart parameters (branch, case)\n- AI connection types (8 types)\n- Workflow activation (activateWorkflow/deactivateWorkflow)\n- n8n_deploy_template\n- n8n_workflow_versions\n- n8n_manage_folders (folder CRUD + workflow placement)\n- n8n_manage_credentials (credential CRUD + schema discovery)\n- n8n_audit_instance (security auditing)\n\n### Templates, Data Tables & Self-Help\nSee [OPERATIONS_GUIDE.md](OPERATIONS_GUIDE.md) for:\n- search_templates / get_template / n8n_deploy_template examples\n- n8n_manage_datatable (full actions, filter conditions, examples)\n- tools_documentation, ai_agents_guide, n8n_health_check\n\n---\n\n## Template Usage\n\nThe 2,700+ template library has three tools: `search_templates` (modes `query`/`by_nodes`/`by_task`/`by_metadata`), `get_template` (modes `structure`/`full`), and `n8n_deploy_template` (deploys to your instance with `autoFix`/`autoUpgradeVersions`, returns workflow ID + required credentials + fixes applied).\n\nSee [OPERATIONS_GUIDE.md](OPERATIONS_GUIDE.md) for full search/get/deploy examples.\n\n---\n\n## Running Workflows\n\n`n8n_test_workflow` has one required parameter (`workflowId`) and a `method` that picks the path:\n\n| `method` | Backend | What it does |\n|---|---|---|\n| `auto` (default) | Public API | Detects a webhook/form/chat trigger and fires it over HTTP — the workflow must be **active**. No such trigger → it reports that the workflow cannot be triggered and names the methods below. **`auto` never runs anything through n8n's MCP server.** |\n| `trigger` | Public API | Same HTTP path, requested explicitly. |\n| `prepare` | n8n's MCP server | Read-only: lists the nodes that need pinned data. |\n| `pinned` | n8n's MCP server | Runs the workflow with `pinData` standing in for trigger, credentialed and HTTP Request nodes, and waits. Every other node still runs. A run that finishes in `error`/`crashed`/`canceled` comes back as `EXECUTION_FAILED` with the `executionId`. |\n| `direct` | n8n's MCP server | Starts a run and returns once it has started; nothing is pinned, so every node runs. `message` or `data`/`headers` are forwarded to the trigger as input. |\n\n- The last three need `N8N_MCP_ACCESS_TOKEN` (n8n 2.34+) and the workflow's \"Available in MCP\" setting.\n- `pinData` is keyed by node **name**, and every value is an array of items wrapped as `{\"json\": {...}}` — `{\"Webhook\": [{\"json\": {\"id\": \"123\"}}]}`, never a flat object. It must be non-empty.\n- `triggerNodeName` picks the trigger node to start from (defaults to the detected one; n8n requires it whenever inputs are given).\n- **Both run methods execute the workflow's nodes for real.** `direct` runs every node; `pinned` pins only trigger nodes, nodes with credentials and HTTP Request nodes, so Code, Set, If and credential-free I/O (Execute Command, file read/write) still run. Confirm with the user before running a workflow that writes anywhere.\n- `executionMode` applies to `direct`: `manual` (default) or `production`. It changes the execution context, not whether the run has side effects — a production run goes through the production execution path and is recorded as one. Only pass it when the user asked for one.\n- `timeoutMs` is the client deadline for the official call (5000-600000; default 30000 for `prepare`, 300000 for `pinned`/`direct`).\n- `direct` returns as soon as the run starts, so it reports success with an `executionId` regardless of how the run ends — poll `n8n_executions({action: \"get\", id: executionId})` for the outcome. A dispatch n8n refuses outright comes back as `OFFICIAL_MCP_ERROR`, not `EXECUTION_FAILED`.\n- A workflow whose \"Available in MCP\" setting is off answers `WORKFLOW_NOT_EXPOSED`; `exposeToMcp: true` turns the setting on and retries once. That is a visible, persistent change — ask the user first, and note that enabling it is itself a workflow update, so it can overwrite a concurrent UI edit.\n\nSuccessful and routed responses state `method` and `backend` (`public-api` or `official-mcp`); an envelope rejected on argument validation may carry neither.\n\nSee [WORKFLOW_GUIDE.md](WORKFLOW_GUIDE.md#n8n_test_workflow-running-workflows) for runnable examples of each method.\n\n---\n\n## Version History\n\n`n8n_workflow_versions` reads two independent histories, selected with `source`:\n\n- `source: \"local\"` (default) — the snapshots n8n-mcp takes before it changes a workflow. Any n8n version, no token, ids are numbers. Blind to edits made in the n8n UI. The only source that supports `delete` and `prune`.\n- `source: \"native\"` — n8n's own workflow history, the same list the UI shows, including edits made by people. Needs `N8N_MCP_ACCESS_TOKEN` (n8n 2.34+; the native `diff` needs 2.36, where `get_workflow_versions_diff` shipped) and the workflow's \"Available in MCP\" setting; ids are opaque strings; `list` is capped at 50 with an `offset`; `delete` and `prune` are refused with `MODE_NOT_SUPPORTED_FOR_SOURCE` (n8n owns that retention). Native rollback is not pre-validated — `validateBefore` is accepted and ignored.\n\n`mode: \"diff\"` compares two versions (`versionId` + `toVersionId`, both from the same source and workflow). A local diff (`data.format: \"n8n-mcp\"`) reports added/removed/modified nodes as node **IDs**; a native diff (`data.format: \"n8n\"`) is n8n's own payload with field-level before/after values. Branch on `data.format` rather than assuming field names.\n\nNative modes hit the same consent gate as the routed run methods: a workflow whose \"Available in MCP\" setting is off answers `WORKFLOW_NOT_EXPOSED`, and re-running with `exposeToMcp: true` turns that setting on and retries once (the response then carries `exposedToMcp: true`). It is a visible, persistent change to the workflow — ask the user before passing it. `timeoutMs` (5000-600000) is the client deadline for the native call.\n\nSee [WORKFLOW_GUIDE.md](WORKFLOW_GUIDE.md#n8n_workflow_versions-version-control) for every mode with runnable examples of both sources.\n\n---\n\n## Data Table Management\n\n`n8n_manage_datatable` is the MCP tool for managing data tables and rows from *outside* a workflow (table actions `createTable`/`listTables`/`getTable`/`updateTable`/`deleteTable`; row actions `getRows`/`insertRows`/`updateRows`/`upsertRows`/`deleteRows`, with filtering, pagination, and `dryRun`). Don't confuse it with the in-workflow `nodes-base.dataTable` node, which reads/writes rows *during execution* (see [n8n-node-configuration → OPERATION_PATTERNS.md](../n8n-node-configuration/OPERATION_PATTERNS.md#data-table-nodes-basedatatable)). Rule of thumb: MCP tool to set up a table once, workflow node to read/write on every execution. `deleteRows` requires a filter; use `dryRun: true` before bulk changes.\n\n**Column actions** — `addColumn`, `deleteColumn`, `renameColumn` — change an existing table's columns, which the Public API cannot do; they run through n8n's MCP server and need `N8N_MCP_ACCESS_TOKEN` (n8n 2.34+). `addColumn` takes `column: {name, type}` (name starts with a letter, letters/digits/underscores only, at most 63 chars; type is `string`, `number`, `boolean` or `date`); `deleteColumn`/`renameColumn` take the `columnId` from `getTable`, and `renameColumn` puts the new column name in `name`. They address the table by project: `projectId` is resolved automatically when exactly one project is accessible, otherwise the call returns `PROJECT_REQUIRED` and lists the candidates — pass `projectId` (from `n8n_list_catalog({kind: \"projects\"})`) to skip resolution. Renaming the *table* is not a column action: use `updateTable` on the Public API.\n\n**`deleteColumn` drops the column's values along with the column, and there is no undo.** That bites hardest where you'd least expect it: a column's type cannot be changed after creation, so \"make this column a number\" really means drop-and-re-add, which throws away everything in it. Read the values out with `getRows` first if they matter, and confirm with the user before dropping a populated column.\n\nSee [OPERATIONS_GUIDE.md](OPERATIONS_GUIDE.md) for all actions, filter conditions, and examples.\n\n---\n\n## Workflow Folders\n\n`n8n_manage_folders` organizes workflows into folders (actions `create`/`list`/`get`/`rename`/`move`/`delete`; n8n 2.19+, registered free Community tier and up). `projectId` defaults to `'personal'`. Placing workflows happens in the *workflow* tools: `parentFolderId` on `n8n_create_workflow`, or the `moveToFolder` operation of `n8n_update_partial_workflow` (both n8n 2.32+; `null` = project root). Two things to internalize: a workflow's folder is **write-only** in n8n's API (verify placement via a folder's `get` counts, never by reading the workflow), and `delete` without `transferToFolderId` **archives** the folder's workflows (`transferToFolderId: \"0\"` moves them to the project root instead, keeping them active).\n\nSee [WORKFLOW_GUIDE.md](WORKFLOW_GUIDE.md) for all actions, list filters/counts, and the delete semantics.\n\n---\n\n## Credential Management\n\n`n8n_manage_credentials` is the unified credential tool: actions `list`, `get`, `create`, `update`, `delete`, `getSchema`. It never returns secrets — `get`/`create`/`update` strip the `data` field. Use `getSchema` before `create` to discover required fields. The optional `includeUsage: true` flag (on `list`/`get`) reverse-scans workflows and attaches `usedIn: [{id, name, active}]` + `usageCount` — use it before deleting or rotating a credential to see what breaks (it triggers a full client-side scan, caps at 5000 workflows, excludes archived, and degrades to a `usageScanError` field on failure).\n\nSee [WORKFLOW_GUIDE.md](WORKFLOW_GUIDE.md) for all actions, the includeUsage shape, security notes, and the safe delete/rotate workflow.\n\n---\n\n## Agents\n\nThe three tools in this section exist only for n8n's instance-level MCP server (a separate endpoint from the Public API). `n8n_manage_agents` and `n8n_explore_node_resources` need `N8N_MCP_ACCESS_TOKEN`; `n8n_list_catalog` works without it and uses the token only for its team-project fallback. Other tools route individual operations through the same server — `n8n_test_workflow` `prepare`/`pinned`/`direct`, `n8n_workflow_versions` `source: \"native\"`, the `n8n_manage_datatable` column actions — as described in their own sections; see \"Tool Availability\" below.\n\n- `n8n_manage_agents` — create, configure, validate, run and publish persisted n8n Agents (a standalone assistant artifact: model, instructions, tools, skills, tasks, memory, channels — not the AI Agent workflow node). Actions: `reference`, `search`, `get`, `create`, `mutate`, `validate`, `call`, `publish`, `unpublish`, `revert`, `versions`, `delete`, `discover_assets`, `verify_mcp_server`, `update_integration`. Start with `action: \"reference\"`, then `discover_assets` → `create` → `mutate` (one resource at a time, always the latest hash — n8n returns it as `configHash` and expects it back as `args.baseConfigHash`; a stale one comes back as `STALE_CONFIG`) → `validate`. `publish` only on explicit request; `call` runs the agent with real credentials and tools and may return `approvals[]` for the human to decide. `timeoutMs` is a top-level parameter (default 30000, 180000 for `call`), not part of `args`. Needs n8n **2.34+** with the agents module; on 2.36.x the agents runtime rejects `azureOpenAiApi`/`aws` credentials. Envelope error codes: `NOT_CONFIGURED`, `INVALID_ARGS`, `STALE_CONFIG`, `AGENT_NOT_RUNNABLE`, `AGENT_TOOL_ERROR` (a custom tool that failed to compile, or an unknown `agentId`), plus the shared `OFFICIAL_MCP_*` family (`AUTH_FAILED`, `NOT_ENABLED`, `RATE_LIMITED`, `TOOL_UNAVAILABLE`, `URL_REJECTED`, `TIMEOUT`, `TRANSPORT_ERROR`, `ERROR`). See **n8n-agents** skill's \"Persisted n8n Agents\" section for the full workflow.\n- `n8n_explore_node_resources` — resolve the real values behind a node's `loadOptions` dropdown or resource-locator `listSearch` (Slack channels, Google Sheets tabs, model lists) using a live credential, instead of guessing an ID. Use it when `get_node` (`standard` detail) shows `dynamicOptions: {methodName, methodType, dependsOn}` on a property. **Six parameters are required and none of them are inferred:** `nodeType` (LONG form), `version` (the node `typeVersion` the method belongs to), `methodName` and `methodType` copied verbatim from `dynamicOptions`, and `credentialType` plus a `credentialId` of that type from `n8n_manage_credentials({action: \"list\"})`. Whatever the method `dependsOn` goes in `currentNodeParameters`, resource-locator values keeping their `{__rl: true, mode: \"id\", value: \"…\"}` shape. Each result's `value` is what belongs in the workflow parameter; `name` is display text only.\n- `n8n_list_catalog` — list instance-level `projects` (personal project marked, gives `projectId` for `n8n_manage_agents`/`n8n_manage_datatable`) or `tags`. Works without the token via the Public API; with it configured, falls back to the official MCP server for team projects when the Public API's licence gate refuses (`teamProjectsEnabled` reports which).\n\n---\n\n## Security & Audit\n\n`n8n_audit_instance` combines n8n's built-in audit (categories `credentials`/`database`/`nodes`/`instance`/`filesystem`) with a custom deep scan (`hardcoded_secrets`, `unauthenticated_webhooks`, `error_handling`, `data_retention`). All parameters optional: `categories`, `includeCustomScan` (default `true`), `customChecks`, `daysAbandonedWorkflow`. Detected secrets are masked (first 6 + last 4 chars). Output is an actionable markdown report — summary table, findings by workflow, and a Remediation Playbook split into auto-fixable / requires-review / requires-user-action.\n\nSee [WORKFLOW_GUIDE.md](WORKFLOW_GUIDE.md) for the two scanning approaches, examples, and remediation types in full.\n\n---\n\n## Self-Help Tools\n\n- `tools_documentation()` — overview of all tools; `tools_documentation({topic, depth: \"full\"})` for a specific tool. Code node guides via topics `javascript_code_node_guide` / `python_code_node_guide`.\n- **AI agent guide** — `tools_documentation({topic: \"ai_agents_guide\", depth: \"full\"})` (no standalone tool); returns architecture, connections, tools, validation, best practices.\n- `n8n_health_check()` — quick check; `n8n_health_check({mode: \"diagnostic\"})` returns status, env vars, tool status, API connectivity. Both modes also return an **`officialMcp`** block — `{configured, endpoint, reachable, toolCount, agentTools}` — the preflight for everything gated on `N8N_MCP_ACCESS_TOKEN`: the agent tools, `n8n_test_workflow`'s routed methods, native version history, the data-table column actions. Read it once before reaching for any of them, rather than discovering the gap through a `NOT_CONFIGURED` envelope mid-task.\n\nSee [OPERATIONS_GUIDE.md](OPERATIONS_GUIDE.md) for examples.\n\n---\n\n## Tool Availability\n\n**Always Available** (no n8n API needed):\n- search_nodes, get_node\n- validate_node, validate_workflow\n- search_templates, get_template\n- tools_documentation (includes the ai_agents_guide topic)\n\n**Requires n8n API** (N8N_API_URL + N8N_API_KEY):\n- n8n_create_workflow\n- n8n_update_partial_workflow, n8n_update_full_workflow\n- n8n_validate_workflow (by ID)\n- n8n_list_workflows, n8n_get_workflow, n8n_delete_workflow\n- n8n_test_workflow\n- n8n_executions\n- n8n_evaluations (reads: n8n 2.30+ with an API key created on 2.30+; run/cancel: n8n 2.32+ with a key created on 2.32+ — older keys lack the testRun scopes)\n- n8n_deploy_template\n- n8n_workflow_versions\n- n8n_autofix_workflow\n- n8n_manage_datatable\n- n8n_manage_folders (folder CRUD: n8n 2.19+, registered Community tier and up; workflow placement via parentFolderId/moveToFolder: n8n 2.32+)\n- n8n_manage_credentials\n- n8n_audit_instance\n- n8n_list_catalog (works without the token; needs it only for the team-project fallback)\n\n**Requires `N8N_MCP_ACCESS_TOKEN`** (a separate token from n8n Settings → Instance-level MCP, in addition to the Public API credentials above):\n- n8n_manage_agents\n- n8n_explore_node_resources\n- n8n_test_workflow with `method: \"prepare\"`/`\"pinned\"`/`\"direct\"` (also needs the workflow's \"Available in MCP\" setting)\n- n8n_workflow_versions with `source: \"native\"` (also needs the workflow's \"Available in MCP\" setting)\n- n8n_manage_datatable with `addColumn`/`deleteColumn`/`renameColumn`\n\nIf API tools unavailable, use templates and validation-only workflows.\n\n---\n\n## Unified Tool Reference\n\n- **`get_node`** — detail levels (`minimal` ~200 tok / `standard` ~1-2K, RECOMMENDED / `full` ~3-8K, sparingly) and modes (`info` default, `docs`, `search_properties` + `propertyQuery`, `versions`, `compare`, `breaking`, `migrations`). Deep dive in [SEARCH_GUIDE.md](SEARCH_GUIDE.md).\n- **`validate_node`** — modes `full` (default, errors/warnings/suggestions) and `minimal` (required-fields check); profiles `minimal`/`runtime` (default, recommended)/`ai-friendly`/`strict`. Deep dive in [VALIDATION_GUIDE.md](VALIDATION_GUIDE.md).\n\n---\n\n## Performance Characteristics\n\n| Tool | Response Time | Payload Size |\n|------|---------------|--------------|\n| search_nodes | <20ms | Small |\n| get_node (standard) | <10ms | ~1-2KB |\n| get_node (full) | <100ms | 3-8KB |\n| validate_node (minimal) | <50ms | Small |\n| validate_node (full) | <100ms | Medium |\n| validate_workflow | 100-500ms | Medium |\n| n8n_manage_folders | 100-500ms | Small |\n| n8n_manage_credentials | 50-500ms | Small-Medium |\n| n8n_audit_instance | 500-5000ms | Large |\n| n8n_create_workflow | 100-500ms | Medium |\n| n8n_update_partial_workflow | 50-200ms | Small |\n| n8n_deploy_template | 200-500ms | Medium |\n\n---\n\n## Best Practices\n\n### Do\n- For simple workflows (<=5 nodes), use MCP tools directly — don't over-engineer the investigation\n- Use `patchNodeField` for surgical edits to Code node content instead of replacing the entire node\n- Use `get_node({detail: \"standard\"})` for most use cases\n- Specify validation profile explicitly (`profile: \"runtime\"`)\n- Use smart parameters (`branch`, `case`) for clarity\n- Include `intent` parameter in workflow updates\n- Follow search → get_node → validate workflow\n- Iterate workflows (avg 56s between edits)\n- Validate after every significant change\n- Use `includeExamples: true` for real configs\n- Use `n8n_deploy_template` for quick starts\n\n### Don't\n- Use `detail: \"full\"` unless necessary (wastes tokens)\n- Forget nodeType prefix (`nodes-base.*`)\n- Skip validation profiles\n- Try to build workflows in one shot (iterate!)\n- Ignore auto-sanitization behavior\n- Use full prefix (`n8n-nodes-base.*`) with search/validate tools\n- Forget to activate workflows after building\n\n---\n\n## Summary\n\n**Most Important**:\n1. Use **get_node** with `detail: \"standard\"` (default) - covers 95% of use cases\n2. nodeType formats differ: `nodes-base.*` (search/validate) vs `n8n-nodes-base.*` (workflows)\n3. Specify **validation profiles** (`runtime` recommended)\n4. Use **smart parameters** (`branch=\"true\"`, `case=0`)\n5. Include **intent parameter** in workflow updates\n6. **Auto-sanitization** runs on ALL nodes during updates\n7. Workflows can be **activated via API** (`activateWorkflow` operation)\n8. Workflows are built **iteratively** (56s avg between edits)\n9. **Data tables** managed with `n8n_manage_datatable` (CRUD + filtering)\n10. **Folders** managed with `n8n_manage_folders`; workflow placement is write-only (verify via folder counts, not the workflow)\n11. **Credentials** managed with `n8n_manage_credentials` (CRUD + schema discovery)\n12. **Security audits** via `n8n_audit_instance` (built-in + custom deep scan)\n13. **AI agent guide** available via `tools_documentation({topic: \"ai_agents_guide\", depth: \"full\"})`\n\n**Common Workflow**:\n1. search_nodes → find node\n2. get_node → understand config\n3. validate_node → check config\n4. n8n_create_workflow → build\n5. n8n_validate_workflow → verify\n6. n8n_update_partial_workflow → iterate\n7. activateWorkflow → go live!\n\nFor details, see:\n- [SEARCH_GUIDE.md](SEARCH_GUIDE.md) - Node discovery\n- [VALIDATION_GUIDE.md](VALIDATION_GUIDE.md) - Configuration validation + common mistakes\n- [WORKFLOW_GUIDE.md](WORKFLOW_GUIDE.md) - Workflow management\n- [OPERATIONS_GUIDE.md](OPERATIONS_GUIDE.md) - Templates, data tables, self-help tools\n\n---\n\n**Related Skills**:\n- n8n Expression Syntax - Write expressions in workflow fields\n- n8n Workflow Patterns - Architectural patterns from templates\n- n8n Validation Expert - Interpret validation errors\n- n8n Node Configuration - Operation-specific requirements\n- n8n Code JavaScript - Write JavaScript in Code nodes\n- n8n Code Python - Write Python in Code nodes\n","data/skills/n8n-expression-syntax/SKILL.md":"---\nname: n8n-expression-syntax\ndescription: Validate n8n expression syntax and fix common errors. Use when writing n8n expressions, using {{}} syntax, accessing $json/$node variables, troubleshooting expression errors, mapping data between nodes, or referencing webhook data in workflows. Use this skill whenever configuring node fields that reference data from previous nodes — expressions are how n8n passes data between nodes, and getting the syntax wrong is the most common source of workflow errors. Also use when asked whether a complex expression hurts performance.\n---\n\n# n8n Expression Syntax\n\nExpert guide for writing correct n8n expressions in workflows.\n\n---\n\n## Expression Format\n\nAll dynamic content in n8n uses **double curly braces**:\n\n```\n{{expression}}\n```\n\n**Examples**:\n```\n✅ {{$json.email}}\n✅ {{$json.body.name}}\n✅ {{$node[\"HTTP Request\"].json.data}}\n❌ $json.email  (no braces - treated as literal text)\n❌ {$json.email}  (single braces - invalid)\n```\n\n---\n\n## Core Variables\n\n### $json - Current Node Output\n\nAccess data from the current node:\n\n```javascript\n{{$json.fieldName}}\n{{$json['field with spaces']}}\n{{$json.nested.property}}\n{{$json.items[0].name}}\n```\n\n### $node - Reference Other Nodes\n\nAccess data from any previous node:\n\n```javascript\n{{$node[\"Node Name\"].json.fieldName}}\n{{$node[\"HTTP Request\"].json.data}}\n{{$node[\"Webhook\"].json.body.email}}\n```\n\n**Important**:\n- Node names **must** be in quotes\n- Node names are **case-sensitive**\n- Must match exact node name from workflow\n\n### $now - Current Timestamp\n\nAccess current date/time:\n\n```javascript\n{{$now}}\n{{$now.toFormat('yyyy-MM-dd')}}\n{{$now.toFormat('HH:mm:ss')}}\n{{$now.plus({days: 7})}}\n```\n\n### $env - Environment Variables\n\nAccess environment variables:\n\n```javascript\n{{$env.API_KEY}}\n{{$env.DATABASE_URL}}\n```\n\n**Warning**: Some n8n instances have `N8N_BLOCK_ENV_ACCESS_IN_NODE` enabled, which blocks `$env` access entirely. If `$env` returns errors, use alternative approaches:\n- Store values in credentials instead\n- Use a Set node with manually entered values\n- Pass values through webhook query parameters\n\n---\n\n## 🚨 CRITICAL: Webhook Data Structure\n\n**Most Common Mistake**: Webhook data is **NOT** at the root!\n\n### Webhook Node Output Structure\n\n```javascript\n{\n  \"headers\": {...},\n  \"params\": {...},\n  \"query\": {...},\n  \"body\": {           // ⚠️ USER DATA IS HERE!\n    \"name\": \"John\",\n    \"email\": \"john@example.com\",\n    \"message\": \"Hello\"\n  }\n}\n```\n\n### Correct Webhook Data Access\n\n```javascript\n❌ WRONG: {{$json.name}}\n❌ WRONG: {{$json.email}}\n\n✅ CORRECT: {{$json.body.name}}\n✅ CORRECT: {{$json.body.email}}\n✅ CORRECT: {{$json.body.message}}\n```\n\n**Why**: Webhook node wraps incoming data under `.body` property to preserve headers, params, and query parameters.\n\n---\n\n## Common Patterns\n\n### Access Nested Fields\n\n```javascript\n// Simple nesting\n{{$json.user.email}}\n\n// Array access\n{{$json.data[0].name}}\n{{$json.items[0].id}}\n\n// Bracket notation for spaces\n{{$json['field name']}}\n{{$json['user data']['first name']}}\n```\n\n### Reference Other Nodes\n\n```javascript\n// Node without spaces\n{{$node[\"Set\"].json.value}}\n\n// Node with spaces (common!)\n{{$node[\"HTTP Request\"].json.data}}\n{{$node[\"Respond to Webhook\"].json.message}}\n\n// Webhook node\n{{$node[\"Webhook\"].json.body.email}}\n```\n\n### Combine Variables\n\n```javascript\n// Concatenation (automatic)\nHello {{$json.body.name}}!\n\n// In URLs\nhttps://api.example.com/users/{{$json.body.user_id}}\n\n// In object properties\n{\n  \"name\": \"={{$json.body.name}}\",\n  \"email\": \"={{$json.body.email}}\"\n}\n```\n\n---\n\n## When NOT to Use Expressions\n\n### ❌ Code Nodes\n\nCode nodes use **direct JavaScript access**, NOT expressions!\n\n```javascript\n// ❌ WRONG in Code node\nconst email = '={{$json.email}}';\nconst name = '{{$json.body.name}}';\n\n// ✅ CORRECT in Code node\nconst email = $json.email;\nconst name = $json.body.name;\n\n// Or using Code node API\nconst email = $input.item.json.email;\nconst allItems = $input.all();\n```\n\n### ❌ Webhook Paths\n\n```javascript\n// ❌ WRONG\npath: \"{{$json.user_id}}/webhook\"\n\n// ✅ CORRECT\npath: \"user-webhook\"  // Static paths only\n```\n\n### ❌ Credential Fields\n\n```javascript\n// ❌ WRONG\napiKey: \"={{$env.API_KEY}}\"\n\n// ✅ CORRECT\nUse n8n credential system, not expressions\n```\n\n---\n\n## The transform gatekeeper\n\nBefore you add any node — or write any code — to transform data, walk this order and stop at the first that fits:\n\n1. **Expression** (`{{ ... }}`) in the consuming field. Property access, method chains (`.map().filter().join()`), ternaries, string building, Luxon date math — if it's \"take A, produce B\" without intermediate variables, it's an expression. This covers most \"just transform this\" cases.\n2. **Arrow-function IIFE inside an Edit Fields field.** When the logic needs intermediate variables, branching, or comments but still operates on one item, wrap it in an immediately-invoked arrow function right in the field value:\n\n   ```\n   ={{ (() => {\n       const items = $json.line_items;\n       const subtotal = items.reduce((sum, it) => sum + it.price * it.qty, 0);\n       const tax = subtotal * 0.08;\n       return (subtotal + tax).toFixed(2);\n   })() }}\n   ```\n\n   The outer `(...)` brackets the function; the trailing `()` invokes it. Drop either and n8n refuses to run. Inside you get the full expression scope (`$json`, `$('Node')`, `$now`, Luxon) plus `const`/`let`, `if`/`switch`, `try`/`catch`, and regex. No `require`, no `await`.\n3. **Code node — last resort.** Only when you need multi-item aggregation across the whole dataset (`$input.all()`), an allowlisted library, or async work.\n\n**Why the order matters.** It's not style — it's readability and performance. The Code node runs in a sandboxed VM with per-invocation setup and value marshaling — a cold-start cost that can reach 500–1000ms before your logic runs. (It amortizes on warm, high-item-count runs, so treat this as the common-case cost, not a universal constant.) The same logic in an expression or Edit Fields IIFE runs in-process in single-digit milliseconds and skips the sandbox entirely. For pure single-item shaping that's a large gap with no functional difference, and it compounds on hot paths like per-request webhooks. The expression also stays visible in the field that uses it, instead of hiding in an upstream node someone has to open to understand. Reach past a stage only when the input or scope genuinely demands it.\n\n## The Set-node antipattern and branch convergence\n\n### Delete Set nodes that feed one consumer\n\nA Set / Edit Fields node whose only job is to extract a value and hand it to **one** downstream node is dead weight. Inline its expression at the consumer instead.\n\n```\n❌  Webhook → Set { customer_id: {{ $json.body.customer_id }} } → Postgres: WHERE id = {{ $json.customer_id }}\n\n✅  Webhook → Postgres: WHERE id = {{ $('Webhook').item.json.body.customer_id }}\n```\n\nThe Set node adds a hop, more canvas clutter, and a refactor hazard, while doing nothing the consumer couldn't do itself. To remove it cleanly with `n8n_update_partial_workflow`: rewire the connection (`removeConnection` from the Set's source-and-target, `addConnection` straight from source to consumer), `patchNodeField` the consumer's expression to reference the original source by node name, then `removeNode` the Set.\n\n**Quick test:** count how many downstream nodes reference each field the Set produces.\n- **0 or 1** → delete, inline at the consumer.\n- **2+** → it may earn its place.\n\n**Legitimate exceptions** — keep the Set when:\n- **2+ consumers** read the same derived value and the derivation is non-trivial (a name aids readability and you compute it once).\n- **It's a sub-workflow's final Return node**, shaping the output contract. Here the \"single consumer\" is every caller, so the Set *is* the API boundary — and with `Include Other Fields: false` it whitelists the output shape so internal scratch fields don't leak.\n- **You're renaming or whitelisting fields** and want that visible in one place rather than spread across consumer expressions.\n\n### Branch convergence: anchor with a NoOp\n\nWhen branches converge (after IF/Switch/Merge), `$json` becomes \"whichever branch fired last\" — non-deterministic, and a silent source of wrong data. Insert a **NoOp** node at the convergence, name it descriptively (`Combine Inputs`), and have downstream nodes reference it by name:\n\n```\nBranch A ──┐\n           ├─→ [NoOp: Combine Inputs] ──→ downstream uses $('Combine Inputs').item.json.x\nBranch B ──┘\n```\n\nThe NoOp survives refactors: inserting a transform later between it and the consumer doesn't break the `$('Combine Inputs')` reference. (If the branches produce *different* shapes, use a Set node instead of a NoOp to normalize both into one shape — see the exceptions above.)\n\nMore broadly in branchy flows, **prefer `$('Node').item.json.x` over deep `$json.x`.** `$json` breaks the moment an intermediate node is inserted or a node clears item context (Aggregate, Code with Run for All, branching merges); the failure is silent and downstream gets the wrong data with no error. A node-name reference is unambiguous regardless of what sits between source and consumer.\n\n---\n\n## Validation Rules\n\n### 1. Always Use {{}}\n\nExpressions **must** be wrapped in double curly braces.\n\n```javascript\n❌ $json.field\n✅ {{$json.field}}\n```\n\n### 2. Use Quotes for Spaces and Special Characters\n\nField or node names with spaces, diacritics, or special characters require **bracket notation**:\n\n```javascript\n❌ {{$json.field name}}\n✅ {{$json['field name']}}\n\n❌ {{$node.HTTP Request.json}}\n✅ {{$node[\"HTTP Request\"].json}}\n\n// Bracket notation is mandatory for keys with special characters\n✅ {{$json['Gross Price w/o shipment']}}\n✅ {{$json['Cena brutto zł']}}\n```\n\n### 3. Match Exact Node Names\n\nNode references are **case-sensitive**:\n\n```javascript\n❌ {{$node[\"http request\"].json}}  // lowercase\n❌ {{$node[\"Http Request\"].json}}  // wrong case\n✅ {{$node[\"HTTP Request\"].json}}  // exact match\n```\n\n### 4. No Nested {{}}\n\nDon't double-wrap expressions:\n\n```javascript\n❌ {{{$json.field}}}\n✅ {{$json.field}}\n```\n\n---\n\n## Common Mistakes\n\nFor complete error catalog with fixes, see [COMMON_MISTAKES.md](COMMON_MISTAKES.md)\n\n### Quick Fixes\n\n| Mistake | Fix |\n|---------|-----|\n| `$json.field` | `{{$json.field}}` |\n| `{{$json.field name}}` | `{{$json['field name']}}` |\n| `{{$node.HTTP Request}}` | `{{$node[\"HTTP Request\"]}}` |\n| `{{{$json.field}}}` | `{{$json.field}}` |\n| `{{$json.name}}` (webhook) | `{{$json.body.name}}` |\n| `'={{$json.email}}'` (Code node) | `$json.email` |\n\n---\n\n## Working Examples\n\nFor real workflow examples, see [EXAMPLES.md](EXAMPLES.md)\n\n### Example 1: Webhook to Slack\n\n**Webhook receives**:\n```json\n{\n  \"body\": {\n    \"name\": \"John Doe\",\n    \"email\": \"john@example.com\",\n    \"message\": \"Hello!\"\n  }\n}\n```\n\n**In Slack node text field**:\n```\nNew form submission!\n\nName: {{$json.body.name}}\nEmail: {{$json.body.email}}\nMessage: {{$json.body.message}}\n```\n\n### Example 2: HTTP Request to Email\n\n**HTTP Request returns**:\n```json\n{\n  \"data\": {\n    \"items\": [\n      {\"name\": \"Product 1\", \"price\": 29.99}\n    ]\n  }\n}\n```\n\n**In Email node** (reference HTTP Request):\n```\nProduct: {{$node[\"HTTP Request\"].json.data.items[0].name}}\nPrice: ${{$node[\"HTTP Request\"].json.data.items[0].price}}\n```\n\n### Example 3: Format Timestamp\n\n```javascript\n// Current date\n{{$now.toFormat('yyyy-MM-dd')}}\n// Result: 2025-10-20\n\n// Time\n{{$now.toFormat('HH:mm:ss')}}\n// Result: 14:30:45\n\n// Full datetime\n{{$now.toFormat('yyyy-MM-dd HH:mm')}}\n// Result: 2025-10-20 14:30\n```\n\n---\n\n## Data Type Handling\n\n### Arrays\n\n```javascript\n// First item\n{{$json.users[0].email}}\n\n// Array length\n{{$json.users.length}}\n\n// Last item\n{{$json.users[$json.users.length - 1].name}}\n```\n\n### Objects\n\n```javascript\n// Dot notation (no spaces)\n{{$json.user.email}}\n\n// Bracket notation (with spaces or dynamic)\n{{$json['user data'].email}}\n```\n\n### Strings\n\n```javascript\n// Concatenation (automatic)\nHello {{$json.name}}!\n\n// String methods\n{{$json.email.toLowerCase()}}\n{{$json.name.toUpperCase()}}\n```\n\n### Numbers\n\n```javascript\n// Direct use\n{{$json.price}}\n\n// Math operations\n{{$json.price * 1.1}}  // Add 10%\n{{$json.quantity + 5}}\n```\n\n---\n\n## Advanced Patterns\n\n### Conditional Content\n\n```javascript\n// Ternary operator\n{{$json.status === 'active' ? 'Active User' : 'Inactive User'}}\n\n// Default values\n{{$json.email || 'no-email@example.com'}}\n```\n\n### Date Manipulation\n\n```javascript\n// Add days\n{{$now.plus({days: 7}).toFormat('yyyy-MM-dd')}}\n\n// Subtract hours\n{{$now.minus({hours: 24}).toISO()}}\n\n// Set specific date\n{{DateTime.fromISO('2025-12-25').toFormat('MMMM dd, yyyy')}}\n```\n\n### String Manipulation\n\n```javascript\n// Substring\n{{$json.email.substring(0, 5)}}\n\n// Replace\n{{$json.message.replace('old', 'new')}}\n\n// Split and join\n{{$json.tags.split(',').join(', ')}}\n```\n\n---\n\n## Performance: expression complexity is (almost) free\n\nA common worry is that a complex `{{ }}` is slow. It isn't — what costs is *how many times* n8n evaluates an expression, not how elaborate each one is.\n\nMeasured on an n8n 2.x instance, an elaborate expression (`sqrt`, `split`, `reduce`, arithmetic) costs the same per item as a trivial `{{ $json.x > 50 }}` — roughly **~0.2 ms/item either way**, because ~90% of that is n8n building the per-item evaluation context, not running your expression.\n\nWhat this means in practice:\n\n- **Don't break a working expression into a chain of nodes for \"speed.\"** Each extra node re-evaluates per item and re-copies all items; one node with one richer expression beats three nodes with simple ones.\n- **An expression (~0.2 ms/item) is ~3× cheaper than a Code node in \"Run Once for Each Item\" mode** (~0.6 ms/item) for the same per-item check — but a Code node in \"Run Once for All Items\" mode is cheaper still (~0.02 ms/item), because it crosses the per-item boundary once instead of N times.\n- This only bites at **thousands of items**; below that it's sub-100 ms. The **n8n Code JavaScript** skill has the full per-item-boundary model.\n\n---\n\n## Debugging Expressions\n\n### Test in Expression Editor\n\n1. Click field with expression\n2. Open expression editor (click \"fx\" icon)\n3. See live preview of result\n4. Check for errors highlighted in red\n\n### Common Error Messages\n\n**\"Cannot read property 'X' of undefined\"**\n→ Parent object doesn't exist\n→ Check your data path\n\n**\"X is not a function\"**\n→ Trying to call method on non-function\n→ Check variable type\n\n**Expression shows as literal text**\n→ Missing {{ }}\n→ Add curly braces\n\n---\n\n## Expression Helpers\n\n### Available Methods\n\n**String**:\n- `.toLowerCase()`, `.toUpperCase()`\n- `.trim()`, `.replace()`, `.substring()`\n- `.split()`, `.includes()`\n\n**Array**:\n- `.length`, `.map()`, `.filter()`\n- `.find()`, `.join()`, `.slice()`\n\n**DateTime** (Luxon):\n- `.toFormat()`, `.toISO()`, `.toLocal()`\n- `.plus()`, `.minus()`, `.set()`\n\n**Number**:\n- `.toFixed()`, `.toString()`\n- Math operations: `+`, `-`, `*`, `/`, `%`\n\n---\n\n## Best Practices\n\n### ✅ Do\n\n- Always use {{ }} for dynamic content\n- Use bracket notation for field names with spaces\n- Reference webhook data from `.body`\n- Use $node for data from other nodes\n- Test expressions in expression editor\n\n### ❌ Don't\n\n- Don't use expressions in Code nodes\n- Don't forget quotes around node names with spaces\n- Don't double-wrap with extra {{ }}\n- Don't assume webhook data is at root (it's under .body!)\n- Don't use expressions in webhook paths or credentials\n\n---\n\n## Related Skills\n\n- **n8n MCP Tools Expert**: Learn how to validate expressions using MCP tools\n- **n8n Workflow Patterns**: See expressions in real workflow examples\n- **n8n Node Configuration**: Understand when expressions are needed\n\n---\n\n## Summary\n\n**Essential Rules**:\n1. Wrap expressions in {{ }}\n2. Webhook data is under `.body`\n3. No {{ }} in Code nodes\n4. Quote node names with spaces\n5. Node names are case-sensitive\n\n**Most Common Mistakes**:\n- Missing {{ }} → Add braces\n- `{{$json.name}}` in webhooks → Use `{{$json.body.name}}`\n- `{{$json.email}}` in Code → Use `$json.email`\n- `{{$node.HTTP Request}}` → Use `{{$node[\"HTTP Request\"]}}`\n\nFor more details, see:\n- [COMMON_MISTAKES.md](COMMON_MISTAKES.md) - Complete error catalog\n- [EXAMPLES.md](EXAMPLES.md) - Real workflow examples\n\n---\n\n**Need Help?** Reference the n8n expression documentation or use n8n-mcp validation tools to check your expressions.\n","data/skills/n8n-code-tool/SKILL.md":"---\nname: n8n-code-tool\ndescription: \"Write JavaScript or Python for the n8n Custom Code Tool (@n8n/n8n-nodes-langchain.toolCode) — the AI-agent-callable tool, NOT the workflow Code node. Use when building a Code Tool attached to an AI Agent, writing code that an LLM will invoke, parsing the `query` input, returning a string result, defining an input schema for structured arguments (specifyInputSchema, jsonSchemaExample, DynamicStructuredTool), or troubleshooting errors like \\\"Wrong output type returned\\\", \\\"No execution data available\\\", \\\"The response property should be a string, but it is an object\\\", \\\"Cannot assign to read only property 'name'\\\", or an AI agent that refuses to call the tool. Covers the critical differences between Code node and Code Tool: return format (string vs `[{json:{...}}]`), unavailability of `$fromAI`/`$input`/`$helpers` in the Code Tool sandbox, naming rules for AI invocation, and when to use `toolWorkflow`/HTTP Request Tool instead.\"\n---\n\n# n8n Custom Code Tool\n\nExpert guidance for writing code inside `@n8n/n8n-nodes-langchain.toolCode` — the tool an AI Agent can invoke, **not** the regular workflow Code node.\n\n---\n\n## ⚠️ This is NOT the Code node\n\nThe Custom Code Tool looks like a Code node in the editor — same JavaScript editor, similar layout — but it is a **completely different node** from a different package with a **different runtime contract**.\n\n| | Code node | Custom Code Tool |\n|---|---|---|\n| **Node type** | `n8n-nodes-base.code` | `@n8n/n8n-nodes-langchain.toolCode` |\n| **Package** | `n8n-nodes-base` | `@n8n/n8n-nodes-langchain` |\n| **Invoked by** | Previous node (workflow flow) | AI Agent (LangChain) |\n| **Input** | `$input.all()` — item stream | `query` — string or object from LLM |\n| **Return** | `[{json: {...}}]` (items array) | **A string** |\n| **`$fromAI()`** | N/A | **Not available** (see Errors) |\n| **HTTP helper** | `this.helpers.httpRequest` (auth helpers blocked) | Not exposed to the tool sandbox |\n| **State** | Per-run execution data | No `getContext`, no `$getWorkflowStaticData` |\n\n**If you treat it like a Code node, it fails.** The rest of this skill covers the Code Tool's actual contract.\n\n---\n\n## Quick Start\n\n### Minimal JavaScript Code Tool\n\n```javascript\n// `query` is whatever the AI sent (a string by default)\nreturn `You asked: ${query}`;\n```\n\n### Minimal Python Code Tool\n\n```python\n# `_query` is whatever the AI sent (a string by default)\nreturn f\"You asked: {_query}\"\n```\n\n### Essential Rules\n\n1. **Return a string.** Numbers are auto-converted. Anything else throws `\"The response property should be a string, but it is an object\"`.\n2. **Input variable is fixed**: `query` (JS), `_query` (Python). You cannot rename it.\n3. **Do NOT use `$fromAI()`** inside the Code Tool sandbox — it throws `\"No execution data available\"`.\n4. **Do NOT use `[{json: {...}}]`** return format — that's for Code nodes. Throws `\"Wrong output type returned\"`.\n5. **Use a descriptive tool name** (letters/numbers/underscores, v1.1+). The agent calls the tool by its name.\n6. **Write a precise description** — the LLM decides whether to invoke the tool based on it.\n\n---\n\n## The Two Input Modes\n\nThe Code Tool has two input shapes, controlled by `specifyInputSchema`:\n\n### Mode 1: Unstructured (default, `specifyInputSchema: false`)\n\nThe AI passes **a single string** as `query`. If you need multiple fields, the AI has to stuff them into that one string and you parse them out. In practice, LLMs will happily pass a JSON string if your description tells them to.\n\n```javascript\n// Parse a JSON string the AI sent\nlet params;\ntry {\n  params = typeof query === 'string' ? JSON.parse(query) : query;\n} catch (e) {\n  throw new Error('Expected a JSON object. Parser said: ' + e.message);\n}\nconst price = Number(params.price);\nconst months = Number(params.months);\n// ...\nreturn JSON.stringify({ monthly_payment: /* ... */ });\n```\n\n**Pros**: simplest to set up, one field to describe.\n**Cons**: no schema validation — if the LLM forgets a field, the tool throws at runtime.\n\n**Best for**: quick prototypes, tools with one natural input (a question, a URL, a text blob).\n\n### Mode 2: Structured (`specifyInputSchema: true`)\n\nThe tool becomes a LangChain `DynamicStructuredTool`. The LLM sees a typed argument schema and passes a **validated object** as `query`. You access fields directly.\n\n```javascript\n// query is now an object matching your schema\nconst price = query.price;\nconst months = query.months;\nconst residual_percent = query.residual_percent;\n\nconst monthly = computeAnnuity(price, months, residual_percent);\nreturn JSON.stringify({ monthly_payment: monthly });\n```\n\nSchema is defined via either:\n- `schemaType: \"fromJson\"` + `jsonSchemaExample` (n8n v≥1.3) — paste an example JSON, n8n infers the schema\n- `schemaType: \"manual\"` + `inputSchema` — write a full JSON Schema yourself\n\n**Pros**: LLM gets type hints, invalid calls rejected before your code runs, cleaner code.\n**Cons**: a little more setup; requires n8n version with schema support.\n\n**Best for**: production tools with multiple typed parameters (calculators, API wrappers, anything with numeric fields the LLM tends to stringify).\n\n**See**: [INPUT_SCHEMA.md](INPUT_SCHEMA.md) for complete schema setup.\n\n---\n\n## Return Format\n\n**The return value must be a string.** The LLM reads it as the tool's observation.\n\n```javascript\n// ✅ String\nreturn \"42\";\n\n// ✅ Number (auto-converted to string by n8n)\nreturn 42;\n\n// ✅ JSON-encoded structured result (recommended for rich output)\nreturn JSON.stringify({ result: 42, currency: \"SEK\" });\n\n// ❌ Raw object → \"The response property should be a string, but it is an object\"\nreturn { result: 42 };\n\n// ❌ Workflow item format → \"Wrong output type returned\"\nreturn [{ json: { result: 42 } }];\n\n// ❌ Array → \"The response property should be a string, but it is an object\"\nreturn [1, 2, 3];\n```\n\n### Best practice: JSON-stringify structured results\n\nWhen your tool has more than a trivial scalar output, return a JSON string:\n\n```javascript\nreturn JSON.stringify({\n  monthly_payment_sek: 5405,\n  loan_amount: 351920,\n  total_cost_of_credit: 63295\n});\n```\n\nThe LLM parses JSON reliably and can pick the fields it needs to present to the user.\n\n### Error handling: the agent reads your failures\n\nErrors don't just stop the workflow — they go back to the LLM, which usually corrects its call and retries. Use that:\n\n```javascript\n// Option A: throw — n8n surfaces the message to the agent\nif (!isFinite(price)) throw new Error('price must be a number, e.g. 439900');\n\n// Option B: return an error string — agent reads it like any tool result\nif (!isFinite(price)) return JSON.stringify({ error: 'price must be a number, e.g. 439900' });\n```\n\nEither way, write error messages **for the LLM**: state what was wrong and what a valid call looks like. A bare `throw new Error('invalid input')` wastes the retry; an instructive message usually fixes the next call.\n\n---\n\n## Tool Name and Description\n\nThese fields are NOT documentation — they are the **tool contract the LLM sees**. Treat them as prompt engineering.\n\n### Name\n- Must match `[A-Za-z0-9_]+` (v1.1+). No spaces, no hyphens, no emoji.\n- Use a verb-y descriptive name: `calculate_car_loan`, `get_weather`, `search_orders`.\n- The agent calls the tool by this name. `Code Tool` (the default) is useless — the agent won't know when to call it.\n\n### Description\n- Explain **when** to use it and **what** to send.\n- If unstructured mode, **include an example of the JSON string** the LLM should send.\n- If structured mode, the schema speaks for itself — just describe purpose.\n\n**Unstructured example (JSON-in-string pattern):**\n```\nDeterministiskt beräknar månadskostnad för billån. Anropa med EN JSON-sträng:\n{\"price\":439900,\"down_payment\":87980,\"interest_rate\":6.95,\"months\":36,\"residual_percent\":50}\nFält: price (SEK), down_payment (SEK), interest_rate (% per år), months, residual_percent (0-99).\n```\n\n**Structured example (schema-defined):**\n```\nDeterministically computes the monthly car-loan payment given price, down payment, \nannual interest rate, term, and residual percent. Use whenever the user asks for \nmonthly cost, total credit cost, or loan breakdown.\n```\n\n---\n\n## Top Errors and Fixes\n\n### Error 1: `\"There was an error: 'Cannot assign to read only property \\\"name\\\" of object: Error: No execution data available'\"`\n\n**Cause**: you called `$fromAI()` inside the Code Tool sandbox.\n\n**Fix**: `$fromAI()` is a helper for **other** tool-enabled nodes (HTTP Request Tool, SendGrid Tool, `toolWorkflow`, etc.) — it's not exposed inside `toolCode`. Read the AI's input from `query` directly (or use `specifyInputSchema` for structured fields).\n\n### Error 2: `\"Wrong output type returned\"`\n\n**Cause**: you returned a workflow-style array like `[{ json: { ... } }]`. That's the Code **node** contract, not the Code **Tool** contract.\n\n**Fix**: return a string. For structured data, `return JSON.stringify(output)`.\n\n### Error 3: `\"The response property should be a string, but it is an object\"`\n\n**Cause**: you returned a plain object or array.\n\n**Fix**: `JSON.stringify()` the result, or coerce to a string.\n\n### Error 4: AI never calls the tool\n\n**Cause**: tool name is generic (`Code Tool`, `My Tool`) or description doesn't clearly state when to use it.\n\n**Fix**: rename to a verb-y name (`calculate_car_loan`), and rewrite the description to explicitly state the trigger conditions (e.g. \"Use this whenever the user asks about monthly cost\").\n\n### Error 5: AI sends garbage into `query`\n\n**Cause**: unstructured tool with a vague description. The LLM guesses at the format.\n\n**Fix**: either (a) include a concrete JSON example in the description, or (b) switch to `specifyInputSchema: true` so the LLM gets a typed schema.\n\n**See**: [ERROR_PATTERNS.md](ERROR_PATTERNS.md) for full catalog with reproductions.\n\n---\n\n## What's NOT Available in the Sandbox\n\nThe Code Tool sandbox is **narrower** than the Code node sandbox. Don't assume helpers carry over:\n\n| Helper | Code node | Code Tool |\n|---|---|---|\n| `$input.all()`, `$input.first()`, `$input.item` | ✅ | ❌ |\n| `$node[\"NodeName\"]` | ✅ | ❌ |\n| `$json`, `$binary` | ✅ | ❌ |\n| `$fromAI()` | ❌ | ❌ (despite sitting next to an AI agent) |\n| `this.helpers.httpRequest()` | ✅ | ❌ |\n| `DateTime` (Luxon) | ✅ | ✅ (standard in JS sandbox) |\n| `$jmespath()` | ✅ | ❌ |\n| `this.getContext(...)` | ✅ | ❌ |\n| `$getWorkflowStaticData(...)` | ✅ | ❌ |\n\n**Implication**: the Code Tool is for **pure computation**. If you need an HTTP call, an API lookup, or cross-invocation state, use a different tool node:\n- HTTP Request Tool for external API calls\n- `toolWorkflow` (Call Sub-workflow Tool) for multi-step logic with access to the full Code node sandbox\n- MCP / database tools for persistent state\n\n---\n\n## When to Use Code Tool vs Alternatives\n\nUse **Code Tool** when:\n- ✅ Pure deterministic computation (math, parsing, formatting, validation)\n- ✅ Lightweight transformations the LLM shouldn't do itself (precision math, regex)\n- ✅ You want the code inline in the workflow, not in a separate sub-workflow\n\nUse **`toolWorkflow`** (Call Sub-workflow Tool) when:\n- ✅ You need multiple parameters with clean `$fromAI()` typing\n- ✅ You need access to `this.helpers`, credentials, or other nodes\n- ✅ Logic is reusable across agents\n- ✅ You want structured typed inputs WITHOUT writing a JSON Schema\n\nUse **HTTP Request Tool** when:\n- ✅ The tool is fundamentally a single API call\n- ✅ You want per-parameter `$fromAI()` bindings in URL/query/body\n\n**Rule of thumb**: if you find yourself wanting `$fromAI()`, you probably want `toolWorkflow` instead of `toolCode`.\n\n---\n\n## Complete Working Example\n\nA production calculator tool (unstructured, JSON-in-string pattern):\n\n```json\n{\n  \"parameters\": {\n    \"name\": \"calculate_car_loan\",\n    \"description\": \"Computes monthly car-loan payment using an annuity formula with residual/balloon. Call with a single JSON string. Example: {\\\"price\\\":439900,\\\"down_payment\\\":87980,\\\"interest_rate\\\":6.95,\\\"months\\\":36,\\\"residual_percent\\\":50,\\\"setup_fee\\\":695,\\\"monthly_admin_fee\\\":59}. Required: price, down_payment, interest_rate, months, residual_percent. Optional: setup_fee, monthly_admin_fee (default 0).\",\n    \"language\": \"javaScript\",\n    \"jsCode\": \"let params;\\ntry {\\n  params = typeof query === 'string' ? JSON.parse(query) : query;\\n} catch (e) {\\n  throw new Error('Invalid JSON: ' + e.message);\\n}\\n\\nconst price           = Number(params.price);\\nconst down_payment    = Number(params.down_payment);\\nconst interest_rate   = Number(params.interest_rate);\\nconst months          = Number(params.months);\\nconst residual_percent= Number(params.residual_percent);\\nconst setup_fee       = Number(params.setup_fee ?? 0) || 0;\\nconst monthly_admin_fee = Number(params.monthly_admin_fee ?? 0) || 0;\\n\\nif (!isFinite(price) || price <= 0) throw new Error('price must be > 0');\\nif (down_payment < 0 || down_payment >= price) throw new Error('down_payment must be in [0, price)');\\n\\nconst principal = price - down_payment;\\nconst residual  = price * (residual_percent / 100);\\nconst r = interest_rate / 100 / 12;\\nconst growth = Math.pow(1 + r, months);\\nconst base = r === 0\\n  ? (principal - residual) / months\\n  : (principal - residual / growth) * r / (1 - 1 / growth);\\nconst monthly_payment = base + monthly_admin_fee;\\n\\nreturn JSON.stringify({\\n  monthly_payment_sek: Math.round(monthly_payment),\\n  loan_amount: Math.round(principal),\\n  residual_value_sek: Math.round(residual),\\n  total_cost_of_credit: Math.round(monthly_payment * months + residual + setup_fee - principal)\\n});\"\n  },\n  \"type\": \"@n8n/n8n-nodes-langchain.toolCode\",\n  \"typeVersion\": 1.3,\n  \"name\": \"calculate_car_loan\"\n}\n```\n\nWire it into an AI Agent via the `ai_tool` connection type.\n\n---\n\n## Integration with Other Skills\n\n**n8n-code-javascript**: the Code **node** skill. Most JavaScript patterns (arrays, map/filter, DateTime) transfer — but I/O contract is different. Don't copy data-access code.\n\n**n8n-node-configuration**: `specifyInputSchema` is a classic displayOptions-driven conditional field. Use `get_node({detail: \"standard\"})` on `@n8n/n8n-nodes-langchain.toolCode` to see schema-related properties.\n\n**n8n-workflow-patterns**: Code Tool sits inside the \"AI Agent with tools\" pattern. An agent typically has several tools; Code Tool is the \"local compute\" option.\n\n**n8n-validation-expert**: the three Code Tool errors listed above have clear signatures — if validation surfaces \"Wrong output type returned\", you know to switch from array-of-items to a string.\n\n---\n\n## Quick Reference Checklist\n\nBefore deploying a Code Tool:\n\n- [ ] **Node type** is `@n8n/n8n-nodes-langchain.toolCode` (not `nodes-base.code`)\n- [ ] **Tool name** is descriptive, verb-y, snake_case (e.g. `calculate_car_loan`)\n- [ ] **Description** states when to use the tool and (if unstructured) shows a JSON example\n- [ ] **Input** read from `query` (JS) or `_query` (Python)\n- [ ] **No `$fromAI()`** in the code body\n- [ ] **No `$input` / `$json` / `$helpers`** — those aren't in the sandbox\n- [ ] **Return** is a string (use `JSON.stringify()` for structured output)\n- [ ] **Wired** into an AI Agent via `ai_tool` connection\n- [ ] **Tested** with the exact kind of input the LLM will send (JSON in a string, or schema-validated object)\n\n---\n\n## Additional Resources\n\n- [INPUT_SCHEMA.md](INPUT_SCHEMA.md) — structured input (DynamicStructuredTool) in depth\n- [ERROR_PATTERNS.md](ERROR_PATTERNS.md) — full error catalog with causes and fixes\n\n### Official sources\n- [n8n Custom Code Tool docs](https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.toolcode/)\n- [ToolCode source](https://github.com/n8n-io/n8n/blob/master/packages/%40n8n/nodes-langchain/nodes/tools/ToolCode/ToolCode.node.ts) — the sandbox contract\n- [LangChain tool docs](https://js.langchain.com/docs/modules/agents/tools/) — DynamicTool / DynamicStructuredTool\n\n---\n\n**Remember**: the Code Tool is a LangChain tool wearing a Code-node UI. Contract is: **string in, string out**. Everything else follows from that.\n","data/skills/n8n-multi-instance/SKILL.md":"---\nname: n8n-multi-instance\ndescription: Use when an n8n-mcp account targets more than one n8n instance — i.e. the `n8n_instances` tool is available, the user mentions multiple n8n instances or environments (prod vs staging, several teams or clients), a workflow / datatable / credential / execution call returns an unexpected NOT_FOUND or reads data you don't recognize, or a credential create/update/delete is refused with an `INSTANCE_AMBIGUOUS` error. Covers choosing and switching which instance this MCP session targets, verifying the target before high-stakes work — credential writes above all — and recovering from misroutes and ambiguous-write fail-closes. Always consult this skill before operating on a specific instance, before any credential create/update/delete on a multi-instance account, or when a call hits the wrong/empty data or an `INSTANCE_AMBIGUOUS` error.\n---\n\n# Working with multiple n8n instances over MCP\n\nWhen the `n8n_instances` tool is available, the user has **multi-instance mode** on: one MCP\nconnection can reach several n8n instances (e.g. `prod`, `staging`, or one per client/team).\nEvery other n8n tool (`n8n_get_workflow`, `n8n_list_workflows`, `n8n_update_partial_workflow`,\n`n8n_manage_datatable`, `n8n_manage_credentials`, `n8n_executions`, `n8n_test_workflow`, …) runs\nagainst **whichever instance this session is currently targeting**. There is no per-call instance\nargument: you change the target only by switching. Target the wrong instance and a read returns the\nwrong data and a write lands in the wrong place — usually with **no error** (the one exception is an\nambiguous credential write, which fails closed; see below). So target deliberately.\n\nIf the `n8n_instances` tool is **not** present, the account is single-instance: ignore this skill\nand use the n8n tools directly.\n\n## Golden rules\n\nSix rules. Each prevents a class of silent misroute.\n\n1. **Discover first.** Call `n8n_instances({mode:\"list\"})` before acting so you know the instance\n   names and which one is `current`.\n2. **Switch by name to your target** before doing work on a non-default instance:\n   `n8n_instances({mode:\"switch\", name:\"<instance name>\"})`. The match is case-insensitive.\n3. **Switch in its own turn.** Never put a `switch` and a dependent operation in the **same\n   parallel tool-call batch**. Calls in one batch have no guaranteed order, so the dependent call\n   can be resolved against the *previous* instance before the switch's session state is visible.\n   Switch, let it return, *then* operate.\n4. **Verify before high-stakes ops.** Immediately before creating/updating/deleting **credentials**\n   (and before destructive workflow edits), confirm `current` is the instance you intend — primary\n   check is `n8n_instances({mode:\"list\"})`. The system fail-closes only the *ambiguous* credential\n   case (rule 6); an explicit switch to the **wrong** instance still writes there silently, so this\n   check is on you.\n5. **An unexpected `NOT_FOUND` is almost always a wrong-instance misroute, not a deletion.** Don't\n   recreate the object. Re-check the current instance and retry (see Recovery).\n6. **On `INSTANCE_AMBIGUOUS`, switch on *this* session, then retry.** The system is refusing to\n   write a secret because this session never picked a target itself. Comply — run `switch` here to\n   confirm the instance, then retry the write. Don't work around it or retry blindly.\n\n## Core workflow\n\n```\n1. n8n_instances({mode:\"list\"})                      # see available[] + current + default\n2. n8n_instances({mode:\"switch\", name:\"prod\"})       # bind THIS session to \"prod\"\n   → returns { previous, current }; confirm current.name == \"prod\"\n3. (do your work) n8n_list_workflows / n8n_get_workflow / n8n_manage_datatable / ...\n4. Before a credential write or a delete:\n   n8n_instances({mode:\"list\"})  → re-confirm current, THEN n8n_manage_credentials({action:\"create\", ...})\n```\n\nTo move to another instance, just `switch` again. The whole session follows the switch.\n\n## The `n8n_instances` tool\n\nTwo modes (`mode` is required and enum-validated):\n\n- `{mode:\"list\"}` → `{ current, default, available }`, no side effects.\n  - `current` and `default` are each one instance `{ id, name, url, isDefault }` (or `null`).\n  - `available` is every instance, each with an extra `isCurrent` boolean. Match by **`name`**;\n    never hard-code `id`.\n- `{mode:\"switch\", name:\"<name>\"}` → `{ previous, current }`, and binds this session to the named\n  instance. `name` is case-insensitive.\n\n### Error envelope (from the `n8n_instances` tool)\n\nEvery error returns `{ error: \"<CODE>\", message, … }`. The ones you'll actually hit:\n\n| Code | When | What to do |\n|---|---|---|\n| `UNKNOWN_INSTANCE` | `name` matches no instance | Pick a name from the `available` list in the error payload and retry. |\n| `NAME_REQUIRED` | `switch` with no `name` | Re-call with a `name` (the error lists the valid ones in `available`). |\n| `MULTI_INSTANCE_DISABLED` | multi-instance mode is off | There's nothing to switch; use the n8n tools directly. The user can enable it at the n8n-mcp dashboard. |\n| `NO_SESSION` | the request has **neither** an MCP session id **nor** a credential id | A selection has nowhere to land. Reconnect / initialize a session, then switch. |\n| `UNKNOWN_MODE` | `mode` wasn't `list`/`switch` | Use `list` or `switch`. |\n| `INVALID_CONTEXT` | server-side metadata missing | A server bug, not your input — report it. |\n\n> Instance names can never be `default`, `current`, `list`, or `switch` (reserved), so you'll never\n> see an instance literally named after a mode or field.\n\n### `INSTANCE_AMBIGUOUS` (from the credential-write path, not the tool)\n\nA separate, higher-stakes error. It is **not** returned by `n8n_instances` — it's returned by the\nserver when you call `n8n_manage_credentials` to **create/update/delete** a credential and the target\ninstance is ambiguous: this session never switched on its own but inherited a switch made elsewhere\n(a fan-out / reconnect), pointing at a **non-default** instance. Rather than risk writing a secret to\nthe wrong instance, the server **blocks the write** (it never reaches n8n, no quota is charged) and\nreturns:\n\n```json\n{\n  \"error\": \"INSTANCE_AMBIGUOUS\",\n  \"message\": \"… the session issuing this request never switched there itself … Re-run n8n_instances({mode:\\\"switch\\\", name:\\\"…\\\"}) on this session to confirm the target …\",\n  \"lastSelected\": { \"id\": \"…\", \"name\": \"…\" },\n  \"default\":      { \"id\": \"…\", \"name\": \"…\" }\n}\n```\n\n**Fix:** decide which instance you actually want (`lastSelected` is the inherited switch, `default`\nis the account default), run `n8n_instances({mode:\"switch\", name:\"…\"})` on **this** session, then\nretry the write. See rule 6.\n\n## How targeting behaves (mental model)\n\n- A `switch` **binds this session** to the chosen instance. The binding **persists for the rest of\n  the session and survives reconnects, idle, and backend deploys** (~24h, the MCP session lifetime)\n  — you should not need to re-switch before every call.\n- Other sessions / terminals are **independent**: switching here does not move them.\n- One session targets **one instance at a time**. There is no per-call instance argument; you\n  change the target only via `switch`.\n- **Reads and non-credential writes** route to the currently-selected instance, silently — a\n  misroute produces wrong data or a `NOT_FOUND`, not an error.\n- **Credential writes are the one guarded case.** They route the same way, except the server\n  fail-closes the *ambiguous* state (a session that never switched, recovered onto a non-default\n  instance) with `INSTANCE_AMBIGUOUS`. This is a safety net, not a substitute for rule 4: an\n  explicit switch to the wrong instance still writes there.\n- **If your selected instance is deleted** (the user removes it mid-session), the next call silently\n  falls back to your **default** instance — no error. So default's data appearing where you expected\n  another instance's can look like \"my data vanished.\" Re-list to see where you are.\n\n## Recovery playbook\n\n| Symptom | What it usually means | Do this |\n|---|---|---|\n| `INSTANCE_AMBIGUOUS` on a credential create/update/delete | This session never switched itself; the system won't guess which instance to write the secret to | Run `n8n_instances({mode:\"switch\", name:\"<target>\"})` on this session (the error names `lastSelected` and `default` — pick the one you want), then retry the write. Never retry blindly. |\n| `NOT_FOUND` for a workflow/datatable/credential you **know exists** | You're pointed at the wrong instance — **not** that it was deleted | `n8n_instances({mode:\"list\"})` → check `current`. If it's not your target, `switch` and retry. **Do not recreate the object.** |\n| A read returns **empty or unfamiliar** data | Wrong-instance read, or a silent fallback to `default` after your instance was deleted | `n8n_instances({mode:\"list\"})`, confirm `current`, switch if needed, re-read before drawing conclusions. |\n| `UNKNOWN_INSTANCE` on `switch` | The `name` is wrong (typo, or you guessed) | Read the `available` names in the error and switch to one of those. Names are case-insensitive. |\n| `n8n_health_check` reports an `instanceName` you didn't expect | This session is on a different instance than you think | `switch` to the intended instance, then proceed. |\n| Repeated misroutes within one turn | You batched a `switch` with dependent work | Split them: `switch` alone, await the result, then operate one logical step at a time. |\n\nAfter any recovery switch, sanity-check with `n8n_instances({mode:\"list\"})` (read `current`) as the\nprimary signal. `n8n_health_check` also returns the resolved instance under `details.instanceName`,\nbut it can be absent on some paths (legacy/chat), so treat it as a secondary confirmation.\n\n## Credential operations (highest stakes)\n\nCredentials hold live secrets, and a misrouted credential write puts a secret on the **wrong\ninstance**. The server protects the **ambiguous** case automatically — if this session never picked\na target and inherited a switch to a non-default instance, the write fails closed with\n`INSTANCE_AMBIGUOUS` (rule 6) and never reaches n8n. But that net is narrow: a credential write on a\nsession that **did** switch goes through to whatever instance it switched to, with no second\nguess. So:\n\n- **Verify `current` immediately before** `n8n_manage_credentials` create/update/delete — call\n  `n8n_instances({mode:\"list\"})` in the same short sequence, not 10 steps earlier where a later\n  switch could have moved you.\n- **On `INSTANCE_AMBIGUOUS`**, switch on this session to confirm the target, then retry — don't\n  work around it.\n- Credential **reads** (`action:\"list\"`/`\"get\"`/`\"getSchema\"`) are not gated and don't write a\n  secret, but a read off the wrong instance returns the wrong schema or list — so still verify\n  `current` if the result looks wrong.\n- For the `n8n_manage_credentials` tool itself (CRUD shapes, `getSchema` discovery, never inlining\n  secrets into text fields), see `n8n-mcp-tools-expert`.\n\n## Common multi-instance task: copy something between instances\n\nTo recreate a credential or workflow from instance A on instance B:\n\n```\n1. switch → A;  read the source (n8n_manage_credentials get / n8n_get_workflow)\n2. switch → B   (its own call — never batched with the create below)\n3. n8n_instances({mode:\"list\"})  → confirm current == B\n4. create on B  (n8n_manage_credentials create / n8n_create_workflow)\n```\n\nDo each instance's steps in its own turn; never overlap `switch → B` with the create-on-B call\n(rule 3), and switch explicitly on this session before the credential write so it isn't ambiguous\n(rules 4 and 6).\n\n## Quick reference\n\n- See instances + where you are: `n8n_instances({mode:\"list\"})` → `{ current, default, available }`\n- Change target: `n8n_instances({mode:\"switch\", name:\"<name>\"})` — its own turn, then operate\n- Confirm target: `current` from `list` (primary); `details.instanceName` from `n8n_health_check` (secondary, may be absent)\n- `UNKNOWN_INSTANCE` → switch to a name from the error's `available` list, then retry\n- `INSTANCE_AMBIGUOUS` (credential write) → `switch` on this session to confirm the target, then retry\n- Unexpected `NOT_FOUND` → verify the instance, switch, retry; **do not recreate**\n- Before credential writes → re-`list`, confirm `current`, then write (the fail-close only covers the ambiguous case)\n\n## Integration with other skills\n\n- **n8n-mcp-tools-expert** — owns `n8n_manage_credentials` (CRUD + `getSchema`) and the rule that\n  secrets go through the credential system, never text fields. This skill adds the \"which instance?\"\n  layer on top.\n- **using-n8n-mcp-skills** — the router; consult it for which skill owns a given build step.\n","data/skills/n8n-node-configuration/SKILL.md":"---\nname: n8n-node-configuration\ndescription: Operation-aware node configuration guidance. Use when configuring nodes, understanding property dependencies, determining required fields, choosing between get_node detail levels, or learning common configuration patterns by node type. Always use this skill when setting up node parameters — it explains which fields are required for each operation, how displayOptions control field visibility, and when to use patchNodeField for surgical edits vs full node updates.\n---\n\n# n8n Node Configuration\n\nExpert guidance for operation-aware node configuration with property dependencies.\n\n---\n\n## Configuration Philosophy\n\n**Progressive disclosure**: Start minimal, add complexity as needed\n\nConfiguration best practices:\n- `get_node` with `detail: \"standard\"` is the most used discovery pattern\n- 56 seconds average between configuration edits\n- Covers 95% of use cases with 1-2K tokens response\n\n**Key insight**: Most configurations need only standard detail, not full schema!\n\n---\n\n## Core Concepts\n\n### 1. Operation-Aware Configuration\n\n**Not all fields are always required** - it depends on operation!\n\n**Example**: Slack node\n```javascript\n// For operation='post'\n{\n  \"resource\": \"message\",\n  \"operation\": \"post\",\n  \"channel\": \"#general\",  // Required for post\n  \"text\": \"Hello!\"        // Required for post\n}\n\n// For operation='update'\n{\n  \"resource\": \"message\",\n  \"operation\": \"update\",\n  \"messageId\": \"123\",     // Required for update (different!)\n  \"text\": \"Updated!\"      // Required for update\n  // channel NOT required for update\n}\n```\n\n**Key**: Resource + operation determine which fields are required!\n\n### 2. Property Dependencies\n\n**Fields appear/disappear based on other field values**\n\n**Example**: HTTP Request node\n```javascript\n// When method='GET'\n{\n  \"method\": \"GET\",\n  \"url\": \"https://api.example.com\"\n  // sendBody not shown (GET doesn't have body)\n}\n\n// When method='POST'\n{\n  \"method\": \"POST\",\n  \"url\": \"https://api.example.com\",\n  \"sendBody\": true,       // Now visible!\n  \"body\": {               // Required when sendBody=true\n    \"contentType\": \"json\",\n    \"content\": {...}\n  }\n}\n```\n\n**Mechanism**: displayOptions control field visibility\n\n### 3. Progressive Discovery\n\n**Use the right detail level**:\n\n1. **get_node({detail: \"standard\"})** - DEFAULT\n   - Quick overview (~1-2K tokens)\n   - Required fields + common options\n   - **Use first** - covers 95% of needs\n\n2. **get_node({mode: \"search_properties\", propertyQuery: \"...\"})** (for finding specific fields)\n   - Find properties by name\n   - Use when looking for auth, body, headers, etc.\n\n3. **get_node({detail: \"full\"})** (complete schema)\n   - All properties (~3-8K tokens)\n   - Use only when standard detail is insufficient\n\n---\n\n## Configuration Workflow\n\n### Standard Process\n\n1. Identify node type and operation.\n2. Use `get_node` (standard detail is default).\n3. Configure required fields.\n4. Validate configuration.\n5. If a field is unclear → `get_node({mode: \"search_properties\"})`.\n6. Add optional fields as needed.\n7. Validate again.\n8. Deploy.\n\n### Example: Configuring HTTP Request\n\nThe validate-driven loop in practice: start minimal (`method`, `url`, `authentication`), then let each `validate_node` error surface the next required field (`sendBody` for POST → `body` when `sendBody=true`) until valid. Full step-by-step walkthrough in **[OPERATION_PATTERNS.md](OPERATION_PATTERNS.md#worked-example-configuring-http-request-step-by-step)**.\n\n---\n\n## get_node Detail Levels\n\n### Standard Detail (DEFAULT - Use This!)\n\n**✅ Starting configuration**\n```javascript\nget_node({\n  nodeType: \"nodes-base.slack\"\n});\n// detail=\"standard\" is the default\n```\n\n**Returns** (~1-2K tokens):\n- Required fields\n- Common options\n- Operation list\n- Metadata\n\n**Use**: 95% of configuration needs\n\n### Full Detail (Use Sparingly)\n\n**✅ When standard isn't enough**\n```javascript\nget_node({\n  nodeType: \"nodes-base.slack\",\n  detail: \"full\"\n});\n```\n\n**Returns** (~3-8K tokens):\n- Complete schema\n- All properties\n- All nested options\n\n**Warning**: Large response, use only when standard insufficient\n\n### Search Properties Mode\n\n**✅ Looking for specific field**\n```javascript\nget_node({\n  nodeType: \"nodes-base.httpRequest\",\n  mode: \"search_properties\",\n  propertyQuery: \"auth\"\n});\n```\n\n**Use**: Find authentication, headers, body fields, etc.\n\n### Decision Tree\n\n1. Starting a new node config → `get_node` (standard).\n2. Standard has what you need → configure with it. Otherwise continue.\n3. Looking for a specific field → `search_properties` mode. Otherwise continue.\n4. Still need more → `get_node({detail: \"full\"})`.\n\n**Dynamic properties**: when `standard` detail marks a property with `dynamicOptions: {methodName, methodType, dependsOn}`, its real values come from a live `loadOptions`/`listSearch` method, not from bundled docs — don't guess an ID for it. Resolve it with `n8n_explore_node_resources` (needs `N8N_MCP_ACCESS_TOKEN`, n8n 2.34+) and put the returned `value` in the config; `name` is display text only.\n\nAll six parameters are required and none are inferred from each other:\n\n```javascript\nn8n_explore_node_resources({\n  nodeType: \"n8n-nodes-base.googleSheets\",  // LONG form\n  version: 4.5,                              // the node typeVersion the method belongs to\n  methodName: \"getSheets\",                   // copied verbatim from dynamicOptions\n  methodType: \"listSearch\",                  // \"listSearch\" for resource locators, \"loadOptions\" for plain dropdowns\n  credentialType: \"googleSheetsOAuth2Api\",\n  credentialId: \"c2\",                        // from n8n_manage_credentials({action: \"list\"})\n  currentNodeParameters: {                   // whatever dependsOn names, in its real shape\n    documentId: {__rl: true, mode: \"id\", value: \"1AbC…\"}\n  }\n})\n```\n\n`dependsOn` names the parameters the method needs already chosen — pass them in `currentNodeParameters`, keeping resource-locator values in their `{__rl: true, mode, value}` shape, or the method returns nothing useful. `methodName` is case-sensitive and specific to the `nodeType` + `version` pair; a mismatch returns `OFFICIAL_MCP_ERROR` rather than an empty list.\n\n---\n\n## Property Dependencies Deep Dive\n\nFields have `displayOptions` visibility rules: `show`/`hide` blocks where multiple conditions are AND'd and multiple values are OR'd (e.g. `body` shows when `sendBody=true` AND `method IN (POST, PUT, PATCH)`). The three recurring patterns are the boolean toggle (sendBody → body), the operation switch (post vs update show different fields), and type selection (string vs boolean conditions). To find what controls a field, use `get_node({mode: \"search_properties\", propertyQuery: \"...\"})` or `get_node({detail: \"full\"})` — especially when validation flags a field you don't see.\n\nMechanism details, all four dependency patterns, complex flows, nested dependencies, and troubleshooting are in **[DEPENDENCIES.md](DEPENDENCIES.md)** (quick-reference recap under [Quick Reference: displayOptions and Common Dependency Patterns](DEPENDENCIES.md#quick-reference-displayoptions-and-common-dependency-patterns)).\n\n---\n\n## Common Node Patterns\n\n### Pattern 1: Resource/Operation Nodes\n\n**Examples**: Slack, Google Sheets, Airtable\n\n**Structure**:\n```javascript\n{\n  \"resource\": \"<entity>\",      // What type of thing\n  \"operation\": \"<action>\",     // What to do with it\n  // ... operation-specific fields\n}\n```\n\n**How to configure**:\n1. Choose resource\n2. Choose operation\n3. Use get_node to see operation-specific requirements\n4. Configure required fields\n\n### Pattern 2: HTTP-Based Nodes\n\n**Examples**: HTTP Request, Webhook\n\n**Structure**:\n```javascript\n{\n  \"method\": \"<HTTP_METHOD>\",\n  \"url\": \"<endpoint>\",\n  \"authentication\": \"<type>\",\n  // ... method-specific fields\n}\n```\n\n**Dependencies**:\n- POST/PUT/PATCH → sendBody available\n- sendBody=true → body required\n- authentication != \"none\" → credentials required\n\n**Critical: credentials block, node id, typeVersion**\n- **Never set a placeholder credential ID** (e.g. `\"id\": \"REPLACE_ME\"`) — n8n's UI renders a permanently disabled credential selector for unknown IDs. Omit the `credentials` block when the real ID is unknown; the user then gets a normal clickable dropdown.\n- **Node `id` must be a UUID v4**, not a readable slug — the frontend binds forms and the credential component to it.\n- **Don't hardcode old `typeVersion` values** — verify the current version with `get_node` (httpRequest is 4.4+).\n\n### Pattern 3: Database Nodes\n\n**Examples**: Postgres, MySQL, MongoDB\n\n**Structure**:\n```javascript\n{\n  \"operation\": \"<query|insert|update|delete>\",\n  // ... operation-specific fields\n}\n```\n\n**Dependencies**:\n- operation=\"executeQuery\" → query required\n- operation=\"insert\" → table + values required\n- operation=\"update\" → table + values + where required\n\n**Critical: Write operations may return 0 items**\n- INSERT, UPDATE, DELETE can produce 0 n8n output items, depending on the node and operation (raw query execution reliably returns 0 result rows; some database nodes return the affected rows)\n- Set `alwaysOutputData: true` on write-operation nodes to keep downstream chains alive\n- Downstream nodes should use `$('UpstreamNode').all()` instead of `$input` if they need data\n\n### Pattern 4: Conditional Logic Nodes\n\n**Examples**: IF, Switch, Merge\n\n**Structure**:\n```javascript\n{\n  \"conditions\": {\n    \"<type>\": [\n      {\n        \"operation\": \"<operator>\",\n        \"value1\": \"...\",\n        \"value2\": \"...\"  // Only for binary operators\n      }\n    ]\n  }\n}\n```\n\n**Dependencies**:\n- Binary operators (equals, contains, etc.) → value1 + value2\n- Unary operators (isEmpty, isNotEmpty) → value1 only + singleValue: true\n\n---\n\n## Operation-Specific Configuration\n\nRequired fields shift with resource + operation: Slack `post` needs `channel`+`text`, but `update` needs `messageId`+`text` (channel optional) and `channel/create` needs `name`. HTTP `GET` uses `sendQuery`+`queryParameters`; `POST` needs `sendBody`+`body`. IF binary operators (`equals`) need `value1`+`value2`; unary (`isEmpty`) need only `value1` plus auto-added `singleValue: true`. Concrete minimal configs for each in **[OPERATION_PATTERNS.md](OPERATION_PATTERNS.md#operation-specific-configuration-examples)**.\n\n---\n\n## Handling Conditional Requirements\n\nSome fields are required only under certain conditions: HTTP `body` is required when `sendBody=true` AND `method IN (POST, PUT, PATCH, DELETE)`; IF `singleValue` should be `true` when the operator is unary (`isEmpty`, `isNotEmpty`, `true`, `false`) — and auto-sanitization sets it for you. Discover conditional requirements by reading the validation error, searching the property (`get_node({mode: \"search_properties\"})`), or iterating from a minimal config. Worked discovery examples in **[DEPENDENCIES.md](DEPENDENCIES.md#handling-conditional-requirements)**.\n\n---\n\n## Node-Specific Configuration Notes\n\n### SplitInBatches v3\n\n```javascript\n{\n  \"batchSize\": 100,  // Number of items per batch\n  \"options\": {}\n}\n```\n\n**Output wiring**:\n- `main[0]` (done) → Connect to downstream processing (add Limit 1 first)\n- `main[1]` (each batch) → Connect to loop body, then loop back to SplitInBatches input\n\nSee the n8n Workflow Patterns skill for detailed loop and nested loop patterns.\n\n### Google Sheets Node\n\n**Per-item execution**: Each input item triggers a separate API call. If you have 100 items and use a Google Sheets \"Append Row\" node, it makes 100 API calls. To write in bulk, aggregate items in a Code node first, then use a single HTTP Request with the Sheets API.\n\n**Formula columns**: Never use `append` on sheets with formula columns — it overwrites formulas. Instead, use HTTP Request with Google Sheets API `values.update` (PUT) method and a `googleApi` credential.\n\n---\n\n## Configuration Anti-Patterns\n\n### ❌ Don't: Over-configure Upfront\n\n**Bad**:\n```javascript\n// Adding every possible field\n{\n  \"method\": \"GET\",\n  \"url\": \"...\",\n  \"sendQuery\": false,\n  \"sendHeaders\": false,\n  \"sendBody\": false,\n  \"timeout\": 10000,\n  \"ignoreResponseCode\": false,\n  // ... 20 more optional fields\n}\n```\n\n**Good**:\n```javascript\n// Start minimal\n{\n  \"method\": \"GET\",\n  \"url\": \"...\",\n  \"authentication\": \"none\"\n}\n// Add fields only when needed\n```\n\n### ❌ Don't: Skip Validation\n\n**Bad**:\n```javascript\n// Configure and deploy without validating\nconst config = {...};\nn8n_update_partial_workflow({...});  // YOLO\n```\n\n**Good**:\n```javascript\n// Validate before deploying\nconst config = {...};\nconst result = validate_node({...});\nif (result.valid) {\n  n8n_update_partial_workflow({...});\n}\n```\n\n### ❌ Don't: Ignore Operation Context\n\n**Bad**:\n```javascript\n// Same config for all Slack operations\n{\n  \"resource\": \"message\",\n  \"operation\": \"post\",\n  \"channel\": \"#general\",\n  \"text\": \"...\"\n}\n\n// Then switching operation without updating config\n{\n  \"resource\": \"message\",\n  \"operation\": \"update\",  // Changed\n  \"channel\": \"#general\",  // Wrong field for update!\n  \"text\": \"...\"\n}\n```\n\n**Good**:\n```javascript\n// Check requirements when changing operation\nget_node({\n  nodeType: \"nodes-base.slack\"\n});\n// See what update operation needs (messageId, not channel)\n```\n\n---\n\n## Surgical Field Edits with patchNodeField\n\nWhen you need to edit a specific string inside a node field — rather than replacing the whole field — use `patchNodeField` in `n8n_update_partial_workflow`. This is especially useful for:\n\n- Editing code inside Code nodes without re-transmitting the full code block\n- Updating URLs or text in large HTML email templates\n- Fixing typos in JSON bodies or long text fields\n\n```javascript\n// Instead of replacing the entire jsCode field:\nn8n_update_partial_workflow({\n  id: \"wf-123\",\n  operations: [{\n    type: \"patchNodeField\",\n    nodeName: \"Code\",\n    fieldPath: \"parameters.jsCode\",\n    patches: [{find: \"const limit = 10;\", replace: \"const limit = 50;\"}]\n  }]\n})\n```\n\n`patchNodeField` is strict — it errors if the find string isn't found or matches multiple times (unless `replaceAll: true`). This prevents accidental silent failures during configuration updates. See the n8n MCP Tools Expert skill for full syntax and examples.\n\n---\n\n## Best Practices\n\n### Do\n\n1. **Start with get_node (standard detail)**\n   - ~1-2K tokens response\n   - Covers 95% of configuration needs\n   - Default detail level\n\n2. **Validate iteratively**\n   - Configure → Validate → Fix → Repeat\n   - Average 2-3 iterations is normal\n   - Read validation errors carefully\n\n3. **Use search_properties mode when stuck**\n   - If field seems missing, search for it\n   - Understand what controls field visibility\n   - `get_node({mode: \"search_properties\", propertyQuery: \"...\"})`\n\n4. **Respect operation context**\n   - Different operations = different requirements\n   - Always check get_node when changing operation\n   - Don't assume configs are transferable\n\n5. **Trust auto-sanitization**\n   - Operator structure fixed automatically\n   - Don't manually add/remove singleValue\n   - IF/Switch metadata added on save\n\n### ❌ Don't\n\n1. **Jump to detail=\"full\" immediately**\n   - Try standard detail first\n   - Only escalate if needed\n   - Full schema is 3-8K tokens\n\n2. **Configure blindly**\n   - Always validate before deploying\n   - Understand why fields are required\n   - Use search_properties for conditional fields\n\n3. **Copy configs without understanding**\n   - Different operations need different fields\n   - Validate after copying\n   - Adjust for new context\n\n4. **Manually fix auto-sanitization issues**\n   - Let auto-sanitization handle operator structure\n   - Focus on business logic\n   - Save and let system fix structure\n\n---\n\n## Silent-Failure Gotchas by Node Family\n\nSome misconfigurations pass `validate_node` and `validate_workflow` clean, run without error, and quietly do the wrong thing — `get_node` shows the fields exist but not what happens when you omit them. The high-frequency ones:\n\n- **Switch** — no `options.fallbackOutput` ⇒ unmatched items silently dropped.\n- **Merge** — `numberOfInputs` defaults to 2 (extra sources drop); `useDataOfInput` is 1-indexed vs the 0-indexed `connections.<src>.main[idx]` slot (`useDataOfInput: \"N\"` → `main[N-1]`).\n- **Database** — `{{ }}` interpolation into `parameters.query` is SQL injection; use `$1/$2` placeholders + `options.queryReplacement`.\n- **Slack** — Block Kit must be wrapped `={{ { \"blocks\": ... } }}` or it posts as plain text.\n- **Webhook / Respond** — `responseCode` defaults to 200 even on error branches.\n- **Schedule Trigger** — timezone is workflow-level (Workflow Settings), not per-rule.\n\nFull symptom/cause/fix detail (in JSON + `n8n_update_partial_workflow` terms) in **[NODE_FAMILY_GOTCHAS.md](NODE_FAMILY_GOTCHAS.md)**.\n\n---\n\n## Detailed References\n\nFor comprehensive guides on specific topics:\n\n- **[DEPENDENCIES.md](DEPENDENCIES.md)** - Deep dive into property dependencies and displayOptions\n- **[OPERATION_PATTERNS.md](OPERATION_PATTERNS.md)** - Common configuration patterns by node type\n- **[NODE_FAMILY_GOTCHAS.md](NODE_FAMILY_GOTCHAS.md)** - Silent runtime traps by family (Switch, Merge, Database, Slack, Webhook, Schedule)\n\n---\n\n## Summary\n\n**Configuration Strategy**:\n1. Start with `get_node` (standard detail is default)\n2. Configure required fields for operation\n3. Validate configuration\n4. Search properties if stuck\n5. Iterate until valid (avg 2-3 cycles)\n6. Deploy with confidence\n\n**Key Principles**:\n- **Operation-aware**: Different operations = different requirements\n- **Progressive disclosure**: Start minimal, add as needed\n- **Dependency-aware**: Understand field visibility rules\n- **Validation-driven**: Let validation guide configuration\n\n**Related Skills**:\n- **n8n MCP Tools Expert** - How to use discovery tools correctly\n- **n8n Validation Expert** - Interpret validation errors\n- **n8n Expression Syntax** - Configure expression fields\n- **n8n Workflow Patterns** - Apply patterns with proper configuration\n","data/skills/n8n-subworkflows/SKILL.md":"---\nname: n8n-subworkflows\ndescription: Build reusable, composable n8n sub-workflows. Use when extracting shared logic, building anything multi-step or reused across workflows, or any workflow over ~10 nodes — and whenever the user mentions sub-workflows, Execute Workflow, reuse, shared/common logic, modular workflows, \"Define Below\" inputs, waitForSubWorkflow, mode each vs all, or exposing a workflow as an agent tool. Covers typed sub-workflow inputs, all-vs-each execution, verb-first naming for discovery, stateless vs stateful design, and splitting by input shape.\n---\n\n# n8n Sub-workflows\n\nA sub-workflow is a reusable function. An **Execute Workflow Trigger** declares typed inputs, the body does the work, and the last node returns the output. A caller invokes it through an **Execute Workflow** node like any other step.\n\nThat framing buys you the things functions buy you everywhere: encapsulation, reuse, testability, replaceability. It's the primary reuse mechanism in n8n, and it's badly underused. Without it, the same logic gets copy-pasted across workflows — then a bug gets fixed in two places, the third copy gets missed, and your \"identical\" copies quietly drift apart.\n\nThis skill is about when to reach for a sub-workflow, how to define its input/output contract so callers (and agents) can actually use it, how to call it correctly (`all` vs `each`, blocking vs fire-and-forget), and how to name it so it gets found instead of rebuilt.\n\n---\n\n## The two non-negotiables\n\nEverything else is judgement. These two are not.\n\n### 1. Search before you build\n\nBefore you write logic for a generic problem, check whether a sub-workflow already does it. The community MCP can't filter workflows by tag, so the **name is the discovery surface**:\n\n```\nn8n_list_workflows()                          # scan the library\nn8n_get_workflow({ id: \"<candidate>\" })       # read its inputs/outputs + body\n```\n\nIf something fits, use it and tell the user (\"I found `Subworkflow: Parse RFC2822 date` — using that\"). If nothing fits, build it *with a discoverable name* so the next search finds it. The discovery convention (verb-first prefixes) lives in **NAMING_AND_DISCOVERY.md**.\n\n### 2. The Execute Workflow Trigger uses \"Define Below\" with typed fields — not passthrough\n\nThe trigger has two input modes. **Default to \"Define Below\"** with explicit typed fields. Define Below is the only mode that gives callers a schema to fill — it's what lets an AI agent pass values via `$fromAI` and what lets structured callers map fields cleanly. Passthrough has no schema, so the trigger can't be wired as a clean agent tool and structured callers have nothing to bind to.\n\nTwo exceptions, and only two:\n\n- **Binary input.** Typed fields are JSON-only. If the sub-workflow must receive an image/file/PDF, you need passthrough so the `binary` slot flows through.\n- **Zero inputs.** Define Below requires at least one field. A genuinely no-arg operation (\"list active credentials\", \"current count\") has nowhere to put an empty schema, so passthrough is the only option.\n\nOutside those two cases, passthrough is a bug. See \"Inputs and outputs as a contract\" below.\n\n---\n\n## Should this be a sub-workflow?\n\nYou're about to write a chunk of logic. Run it through this:\n\n```\nCould this plausibly be needed in another workflow?\n  └─ Yes → extract.\n\nIs it a generic concern (auth, retry, parsing, formatting, ID generation)?\n  └─ Almost always → extract. These are the canonical reusable sub-workflows.\n\nIs it >5 nodes and conceptually one thing?\n  └─ Probably extract, even if reuse isn't certain. It's better isolated.\n\nIs it one HTTP call with no logic around it?\n  └─ Don't. A sub-workflow that's just trigger → HTTP → return adds a boundary\n     for nothing.\n\nIs it tightly coupled to this one caller's data shape?\n  └─ Don't extract yet — fix the data shape first, or you just relocate the coupling.\n```\n\nThe reasons to extract go beyond reuse:\n\n- **Readability.** The caller shows one node (\"Parse date\") instead of five.\n- **Testability.** Run the sub-workflow alone with pinned input: `n8n_test_workflow({workflowId, method: \"prepare\"})` names the nodes that need data, then `method: \"pinned\"` runs it with what you build (needs `N8N_MCP_ACCESS_TOKEN` and the workflow's \"Available in MCP\" setting — see **n8n-mcp-tools-expert**). A sub-workflow has no HTTP trigger, so the default `method: \"auto\"` cannot run it.\n- **Replaceability.** Swap the implementation without rippling to callers.\n\nA 20-node workflow is fine *if it's mostly a linear sequence of Execute Workflow calls and decisions* — each node has one purpose, and you inspect a section by opening the sub-workflow it calls. A 20-node workflow of inline transformations is not fine. If yours has 15+ nodes and isn't mostly sub-workflow calls and branches, extract more.\n\n---\n\n## Stateless vs. stateful (deliberately)\n\nBoth are first-class. The choice is about intent and what the contract promises.\n\n**Stateless** — input in, output out, no I/O beyond that. The default for pure logic. When you need it again, you call it without worrying about side effects firing.\n\n- `Subworkflow: Parse RFC2822 date` — date string → ISO date or error.\n- `Subworkflow: Compute MRR from subscription` — subscription object → number.\n- `Subworkflow: Format invoice as HTML` — invoice data → HTML string.\n\n**Stateful (deliberate)** — reads or writes external state *behind a clean contract*. This is the repository pattern: the sub-workflow abstracts the storage operation so callers think in domain terms, not SQL.\n\n- `Customer: get by id` — id → customer object or `{ ok: false, error: \"not_found\" }`. Reads the DB.\n- `Customer: write billing record` — record → `{ ok: true, id }`. Writes the DB.\n- `Notify: send to on-call` — channel, message → `{ ok: true, messageId }`. Calls Slack/SMTP.\n\nWhy build these as sub-workflows: callers think `get customer by id` instead of writing the query; you can swap the store (Postgres → Supabase, native node → HTTP) without touching a single caller; and idempotency, retry, and validation get centralized in one place.\n\nWhat to avoid is **accidental state** — a sub-workflow named and described as pure that quietly writes to a log table. That ambushes every caller who reasonably assumed it was safe to retry or compose. Either make the side effect part of the contract (rename it, document it, return its result) or move it out.\n\n---\n\n## Inputs and outputs as a contract\n\nThe trigger's declared fields and the last node's output shape *are* the sub-workflow's API. Treat them like one.\n\n### Declaring typed inputs (Define Below)\n\nEach declared input is a typed parameter the caller fills. Pick types deliberately (`string`, `number`, `boolean`, `array`, `object`) — an agent uses these as the required types when filling tool parameters, and humans rely on them when wiring callers. The trigger node parameters look like this:\n\n```json\n{\n  \"type\": \"n8n-nodes-base.executeWorkflowTrigger\",\n  \"parameters\": {\n    \"workflowInputs\": {\n      \"values\": [\n        { \"name\": \"list_of_ids\",        \"type\": \"array\" },\n        { \"name\": \"include_transcript\", \"type\": \"boolean\" },\n        { \"name\": \"session_id\",          \"type\": \"string\" }\n      ]\n    }\n  }\n}\n```\n\nInside the body, read them as `$json.list_of_ids`, or from anywhere downstream as `$('When Executed by Another Workflow').first().json.<field>` (see **n8n-expression-syntax**).\n\n### The contract rules\n\n- **Document inputs and outputs in the workflow `description`.** Field names, types, purpose, and a few representative keywords. The description is what callers (human and agent) read for the contract, and it's what `n8n_list_workflows` matches against.\n- **Return consistent, natural shapes — not storage shapes.** A sub-workflow that owns a Data Table or an S3 file hides that representation from callers. Arrays return as arrays, objects as objects, dates as ISO strings — regardless of whether the underlying storage was JSON-stringified text. The return contract is the *interface*; the storage layout is *implementation detail*. Common slip: a sub-workflow with a \"fresh\" path (just-computed, natural shape) and a \"cached\" path (just read from a stringified column). Wrong instinct: stringify the fresh path to match the cached one. Right instinct: parse the cached path so both return the natural shape.\n- **Return errors, don't always throw.** For *expected* failures (a parse error, a not-found), return `{ ok: false, error: \"...\" }` so the caller can branch without wiring an error output. Reserve throwing for genuinely unexpected failures — see **n8n-error-handling**.\n- **The contract is frozen once it has callers.** Adding *optional* fields is safe. Renaming or removing a field is dangerous: n8n won't error on an unrecognized input field — the body just sees `undefined`, the caller has no idea, and you get a silent contract break. To change a field, enumerate every caller (`n8n_list_workflows` + inspect each one's Execute Workflow node), migrate them in the same change, and verify with `validate_workflow` and `n8n_get_workflow` before you're done.\n\n### The final Return node — the legitimate Set exception\n\nShape the output with a final **Set / Edit Fields** node, named `Return` or `Return <thing>`. This is the one place a Set node earns its keep against the usual \"don't add a trailing Set node\" advice from **n8n-expression-syntax**: the implicit consumer of a sub-workflow's last node is *every caller*, so an explicit Set makes the return contract visible — a reader sees the whole API by reading one node, and you strip any noise fields the last computation node carried.\n\n---\n\n## Calling sub-workflows: `mode` and `waitForSubWorkflow`\n\nTwo settings on the caller's **Execute Workflow** node decide how the sub-workflow runs.\n\n### `mode`: `all` vs `each`\n\n| `mode` | Sub-workflow runs | Items per run |\n|---|---|---|\n| `all` (default) | once | all N items (flowing per-item through nodes as usual) |\n| `each` | N times | exactly one item per run |\n\nFor a body that just processes items the normal way, the two are equivalent — n8n nodes iterate per-item either way. **The split only matters when the body assumes it sees exactly one item**: a per-run aggregation, \"this is THE customer to act on\" logic, or a final write that should fire once per input. With `all`, that body gets all N items at once and the assumption breaks (you aggregate everyone into one result instead of one-per-input). With `each`, each invocation gets one item and the assumption holds.\n\nSo: when you need per-item iteration, prefer `mode: each` over dropping a Loop Over Items node *inside* the sub-workflow. The mode does the iteration for you, and the body stays simple and single-item.\n\n### `waitForSubWorkflow`: `true` vs `false`\n\n`waitForSubWorkflow` defaults to `true` — the caller blocks until the sub-workflow returns, then continues with its output. Set `options.waitForSubWorkflow: false` to fire-and-forget: the call dispatches, the caller moves on immediately, the sub-workflow runs in the background, and downstream sees no return data.\n\n### The only true parallelization n8n offers\n\n`mode: each` + `waitForSubWorkflow: false` is **the only way to get genuinely concurrent sub-workflow execution**: N items dispatch N runs that execute in parallel (still bounded by per-instance concurrency limits). The caller doesn't know when — or whether — any of them finished, so it's only useful with a separate completion-tracking mechanism, typically a Data Table the sub-workflow updates as it progresses. The full stage → dispatch → poll pattern is in **SUBWORKFLOW_PATTERNS.md** (\"Fire-and-forget parallelization\").\n\n---\n\n## Splitting by input shape (the N+1 pattern)\n\nWhen a sub-workflow has multiple input paths whose contracts *genuinely* differ — binary vs JSON, sync vs async, divergent auth schemes — don't cram them under one trigger with passthrough + an internal Switch. The forcing function is real: passthrough (for binary or zero-input) and Define Below (for typed inputs) are mutually exclusive on a single trigger. The reflex to \"pick passthrough because it's most permissive, then branch inside\" costs you the typed schema (no clean agent tool), grows branch-shape cruft, and turns every new input shape into more branching.\n\nThe fix: for N divergent input contracts, build **N+1 sub-workflows** — one outer per contract, each doing its input-specific prep (validation, fetching, hashing, extraction) and calling **one shared downstream** sub-workflow with a normalized shape. The shared core has a single typed input contract and knows nothing about which outer called it. The worked example (process a paper from an external ID *or* an uploaded PDF) is in **SUBWORKFLOW_PATTERNS.md**.\n\n---\n\n## Sub-workflow as an agent tool\n\nA sub-workflow with a typed Define Below trigger doubles as an AI-agent tool: the agent fills the declared fields via `$fromAI`, the body runs, the result comes back as the tool observation. This is the high-value reason to default to Define Below — passthrough triggers can't expose a fill-able schema.\n\nThe zero-input case still works as a tool: the agent's only decision is whether to invoke. The binary case does *not* wire cleanly as a tool, because agents can't pass binary directly.\n\nFor tool naming, descriptions, and the binary-input workaround, see **n8n-agents**; for the binary handling itself, **n8n-binary-and-data**.\n\n---\n\n## Anti-patterns\n\n| Anti-pattern | What goes wrong | Fix |\n|---|---|---|\n| Duplicating the same logic in three workflows | A bug gets fixed in two places, the third drifts | Extract once to a named sub-workflow |\n| Building a new sub-workflow without searching | The library grows duplicates; future searches find both | `n8n_list_workflows` / `n8n_get_workflow` first |\n| Trigger set to passthrough when not handling binary and not zero-input | No schema → agents can't fill params, structured callers can't bind | Use Define Below with typed `workflowInputs.values` |\n| Zero-input passthrough with no clear-and-document | Body silently reads stray fields from whatever the caller forwarded | Start with a Set (\"Keep Only Set\", no fields) and a sticky noting \"no inputs expected\" |\n| Sub-workflow named/described as pure that quietly writes state | Callers can't reason about retry/idempotency; the side effect ambushes them | Make the side effect part of the contract, or move it out |\n| Sub-workflow with no `description` | Won't be found in future searches; nobody knows what it does | Set `description` with input/output shape + keywords |\n| Name like `Helper 3` / no prefix | Doesn't say what it does, matches no prefix search | Verb-first prefix (`Subworkflow:`, `<Domain>:`, `Tool:`) |\n| `mode: all` on a body that assumes one item | Aggregates all inputs into one result instead of one-per-input | `mode: each` (and skip the internal Loop Over Items) |\n| Renaming a live input field without migrating callers | Callers send the old name → body sees `undefined`, no error anywhere | Migrate every caller in the same change; verify with `validate_workflow` |\n| 30-node workflow with no extraction | Hard to read, test, and replace | Extract logical sections into sub-workflows |\n\n---\n\n## What's NOT available via the community MCP\n\n| Want to do | Reality |\n|---|---|\n| Filter/discover workflows by **tag** | The MCP can't read or filter by tags (UI-only). Discovery is the *name* — use verb-first prefixes and `n8n_list_workflows`. |\n| Catch an **unrecognized input field** | n8n doesn't error on one. The body sees `undefined` and the caller never knows — a silent contract break. Verify field renames by hand across callers. |\n| Set the input mode / fields without a typed trigger | The trigger node itself must declare `workflowInputs.values`. Configure it with `n8n_update_partial_workflow` (`updateNode` / `patchNodeField`); validate with `get_node` / `validate_node`. |\n\nWhat the MCP **can** do: build the sub-workflow and its callers (`n8n_update_partial_workflow` with `addNode` / `addConnection` / `updateNode` / `patchNodeField`), discover existing ones (`n8n_list_workflows`, `n8n_get_workflow`), validate (`validate_workflow`, `n8n_validate_workflow`), test in isolation (`n8n_test_workflow`), inspect runs (`n8n_executions`), back a stateful sub-workflow with a Data Table (`n8n_manage_datatable`), and activate (`activateWorkflow`).\n\n---\n\n## Reference files\n\n| File | Read when |\n|---|---|\n| **SUBWORKFLOW_PATTERNS.md** | `mode: all` vs `each` in depth, splitting by input shape (the N+1 worked example), fire-and-forget parallelization with Data Table polling |\n| **NAMING_AND_DISCOVERY.md** | Naming a new sub-workflow, the verb-first prefix convention, searching for existing ones, writing a discoverable description |\n\n---\n\n## Integration with other skills\n\n- **n8n-workflow-patterns** — use it for the overall shape of the orchestrating workflow; use this skill to decide which sections become sub-workflows.\n- **n8n-mcp-tools-expert** — parameter formats for `n8n_list_workflows`, `n8n_get_workflow`, `n8n_update_partial_workflow`, and `n8n_manage_datatable` (the Data Table behind a stateful sub-workflow and the fire-and-forget poll).\n- **n8n-node-configuration** — `workflowInputs` and the `inputSource` (Define Below vs passthrough) toggle are displayOptions-driven config on the Execute Workflow Trigger.\n- **n8n-expression-syntax** — reading inputs (`$json`, `$('When Executed by Another Workflow')`) and the legitimate final-Set exception both live here.\n- **n8n-error-handling** — expected failures return `{ ok: false, error }`; unexpected ones throw and route through error outputs. A sub-workflow boundary is a natural place to define that line.\n- **n8n-validation-expert** — validate the sub-workflow and its callers; an unrecognized input field won't surface here, so verify field changes manually.\n- **n8n-code-javascript / n8n-code-python** — when a sub-workflow's body is a single Code node, its contract is still the trigger's typed inputs and the returned shape, not the Code node's internals.\n- **n8n-code-tool** — the Custom Code Tool is the *inline* agent-tool option; a sub-workflow tool is the reusable, multi-step one. Pick the sub-workflow when the logic is shared across agents or needs the full Code-node sandbox.\n- **n8n-agents** — wiring a typed sub-workflow as an agent tool, including the zero-input and binary cases.\n- **n8n-binary-and-data** — passthrough triggers for binary input, and why binary can't flow through an agent tool directly.\n- **using-n8n-mcp-skills** — when to consult which skill across a build.\n\n---\n\n## Quick reference checklist\n\nBefore shipping a sub-workflow:\n\n- [ ] **Searched first** with `n8n_list_workflows` / `n8n_get_workflow` — it doesn't already exist\n- [ ] **Trigger uses Define Below** with typed `workflowInputs.values` (unless binary or zero-input)\n- [ ] **Zero-input passthrough** (if used) starts with a \"Keep Only Set\" Set node + a sticky noting no inputs\n- [ ] **Name** has a verb-first prefix (`Subworkflow:`, `<Domain>:`, `Tool:`)\n- [ ] **Description** documents input/output shape and carries searchable keywords\n- [ ] **Returns a natural, consistent shape** via a final `Return` Set node — not a storage shape\n- [ ] **Expected failures** return `{ ok: false, error }`; only unexpected ones throw\n- [ ] **Caller `mode`** is `each` if the body assumes a single item (not an internal Loop Over Items)\n- [ ] **`waitForSubWorkflow`** is set deliberately (`false` only with a completion-tracking mechanism)\n- [ ] **Stateful sub-workflows** declare their side effect in name + description — no accidental state\n- [ ] **Validated** with `validate_workflow`; tested in isolation with `n8n_test_workflow`\n\n---\n\n**Remember**: a sub-workflow is a function. Its API is the trigger's typed inputs and the last node's output shape — make both explicit, name it so it's found, and call it with the `mode` its body expects. A passthrough trigger that isn't for binary or a zero-arg op, or a name nobody can search, is how a reusable function quietly becomes the next duplicate.\n","data/skills/n8n-error-handling/SKILL.md":"---\nname: n8n-error-handling\ndescription: Wire n8n error handling so failures are loud, structured, and recoverable. Use when building any webhook/API workflow, a scheduled or unattended workflow, or any path where a silent failure would drop user-visible work — and whenever the user mentions error handling, onError, continueErrorOutput, error branches/outputs, retries, retryOnFail, Respond to Webhook status codes, 4xx/5xx, Error Trigger, or \"my workflow fails silently\". Covers per-node error outputs and wiring, retry/self-healing, error-trigger workflows, and 4xx/5xx response shapes.\n---\n\n# n8n Error Handling\n\nBy default, when an n8n node throws, the **whole workflow halts**. For an interactive run you're watching, that's fine — you see the red node and fix it. For anything unattended (a webhook API, a cron job, a queue worker, an agent tool), it's the wrong default: the caller gets a timeout or an empty 500, the operator gets no alert, and the symptom is \"the integration just stopped working\" with no log and no clue.\n\nThis skill is about making failures **loud, structured, and recoverable** — and, best case, **self-healing** so transient blips never reach a human at all.\n\nThe two ideas that prevent most silent failures:\n\n- **Per-node error outputs** — a node's failure routes down a second output you control, instead of killing the run.\n- **A workflow-level error workflow** — a catch-all that fires for anything that escapes per-node handling (timeouts, crashes between nodes, unwired failures).\n\n---\n\n## When you actually need this\n\n| Workflow shape | Error handling posture |\n|---|---|\n| Webhook / API (anything with `Respond to Webhook`) | **Required.** Every fallible node's error output wired; status code matches cause. |\n| Scheduled / cron / queue worker / agent tool (unattended) | **Required.** A workflow-level error workflow, plus `retryOnFail` on network nodes. |\n| Internal one-off you run and watch yourself | **Optional.** Default `onError: \"stopWorkflow\"` is fine — you'll see the red node and re-run. |\n\nThe dividing line: **if anyone other than you sees the output** — a downstream system, an end user, an on-call engineer — the failure has to be handled, not swallowed. If you're the only watcher and the cost of failure is \"I notice and re-run\", looser is fine.\n\n---\n\n## The #1 silent trap: per-node error output is a TWO-step setup\n\nThis is the single most common way an n8n workflow \"handles\" errors while actually swallowing them. Routing a node's failure to a handler takes **two** changes, and doing only one looks complete but misbehaves:\n\n1. **Set `onError: \"continueErrorOutput\"`** on the node. This is what *creates* the second output. Without it, `main[1]` doesn't exist no matter what you wire.\n2. **Wire that error output** (`connections.<node>.main[1]`, i.e. `sourceIndex: 1`) to a real handler. Without a target, the error data is emitted into the void.\n\nGet one without the other and you hit a failure mode:\n\n| What you did | What happens at runtime |\n|---|---|\n| `onError` set, error output **not** wired | Error data is silently discarded. Downstream doesn't fire. The dashboard shows the run as **succeeded**. Worst case — no error logged anywhere. |\n| Error output wired, `onError` **not** set | The slot never fires; the handler is unreachable. On failure the workflow just **halts** (default `stopWorkflow`). |\n| Both done | Failure routes down `main[1]` to your handler. ✅ |\n\n### Doing both with `n8n_update_partial_workflow`\n\n```javascript\n// 1) Turn on the error output (creates main[1])\n{ type: \"updateNode\", nodeName: \"HTTP Request\",\n  changes: { onError: \"continueErrorOutput\" } }\n\n// 2) Wire the error output to a handler. sourceIndex: 1 = the error output.\n{ type: \"addConnection\",\n  source: \"HTTP Request\",\n  target: \"Handle Error\",\n  sourceIndex: 1 }\n```\n\n`sourceIndex: 0` is the success path, `sourceIndex: 1` is the error path. (For IF nodes the aliases `branch: \"true\"`/`\"false\"` map to index 0/1; for a generic fallible node, use the explicit `sourceIndex: 1`.)\n\n**Then verify.** This trap doesn't surface in `validate_workflow` — a half-wired error output validates clean. Pull the workflow with `n8n_get_workflow` and confirm **both** halves:\n\n- The node's `onError` is `\"continueErrorOutput\"`.\n- `connections[\"HTTP Request\"].main[1]` contains your handler.\n\nValid `onError` values:\n\n| Value | Effect |\n|---|---|\n| `\"stopWorkflow\"` (default) | Error halts the whole workflow. |\n| `\"continueRegularOutput\"` | Error item flows out the **normal** output. Rare, usually wrong — downstream gets error-shaped data and keeps going. |\n| `\"continueErrorOutput\"` | Error item flows out the **separate** error output (`main[1]`). The one you wire. |\n\nFull failure-mode catalog, fan-in/fan-out shapes, and verification: **NODE_ERROR_OUTPUTS.md**.\n\n---\n\n## Self-healing first: `retryOnFail` before you wire error paths\n\nBefore you build error branches, absorb the transient failures so they never reach those branches. On **any node that calls a network service** — HTTP Request, comms (Gmail/Slack/Discord), databases, AI nodes, third-party integrations — set node-level retry:\n\n```javascript\n{ type: \"updateNode\", nodeName: \"HTTP Request\",\n  changes: {\n    retryOnFail: true,\n    maxTries: 3,\n    waitBetweenTries: 5000   // ms\n  } }\n```\n\nWhy this comes **first**: a 429 or a brief upstream hiccup will retry and usually succeed on its own. The error output then fires only on *real, persistent* failures — so your 5xx responses and on-call alerts reflect actual problems instead of noise.\n\nEngine limits to know: retry fires on **any** error (there's no per-status-code filter), `maxTries` caps at 5, and `waitBetweenTries` caps at 5000ms — so 5000 is both the max and a sensible default. See **n8n-node-configuration** (NODE_FAMILY_GOTCHAS.md) for node-specific notes.\n\n---\n\n## API workflows: the canonical shape\n\nA webhook-triggered workflow that responds to its caller has one rule that overrides everything else: **no hanging branches**. Every path — success and every error — must end at a `Respond to Webhook`, or the caller sits there until it times out.\n\n```\nWebhook (responseMode: \"responseNode\")\n  ├── validate input → process → Respond (200, body)\n  └── (any fallible node's error output → sourceIndex 1)\n            → Respond (4xx/5xx, structured error body)\n            → optional: log full error privately / notify\n```\n\nThree things make this work:\n\n1. **Fan-in to one error responder.** Many fallible nodes can route their `main[1]` to a single `Respond` node. Keeps the graph readable.\n2. **Validation failures (4xx) are checked *upstream*, not via error outputs.** A missing field isn't a node *crashing* — it's an expected outcome with a known response. Branch on it with IF/Switch (or the schema validator below) and return 400/401/403/404 directly. Error outputs are for *unexpected* failures (5xx).\n3. **`responseCode` defaults to 200 — even on error branches.** This is its own silent trap (see RESPONSE_SHAPES.md and **n8n-node-configuration** NODE_FAMILY_GOTCHAS.md): an error branch that returns 200 with an error body looks like success to the caller's HTTP client, so their error handling never fires. Set `responseCode` explicitly on every Respond node.\n\n### Input validation: the Set-node schema validator\n\nFor any endpoint doing structured input validation, run the check as an IIFE inside a single **Set** node rather than a chain of IF/Switch nodes per field. One node validates the whole payload, returns `{ valid, validationError, details, requiredSchema }`, and an IF branches on `valid` → your logic (200) or a 400 Respond that echoes the schema back so the caller can self-correct. It's also dramatically faster than a recursive validator in a Code node + sub-workflow. The full pattern, the constraint cookbook, and the expression-escaping gotchas live in **API_WORKFLOWS.md**.\n\n---\n\n## Response shapes: map cause → status code\n\nA 5xx with `text/plain \"Internal Server Error\"` is technically an error response and practically useless. And not every failure is a 5xx. **Match the status code to *why* the request failed**, because the caller branches on it: their monitoring alerts on 5xx (your fault) but not 4xx (their fault), and 5xx suggests \"retry\" while 4xx suggests \"don't\".\n\n**The common mistake:** wiring everything — including bad input — to one `Respond` that returns 500 `internal_error`. Now the caller can't tell their bug from your outage, and your error rates can't separate real incidents from client noise.\n\n| Cause | Status | `error` code | Where it's handled |\n|---|---|---|---|\n| Required field missing / wrong type | 400 | `validation_error` | Upstream check (schema validator / IF), not error output |\n| Auth missing or invalid | 401 | `unauthorized` | Upstream check |\n| Authenticated but not allowed | 403 | `forbidden` | Upstream check |\n| Resource ID valid in request, absent in your data | 404 | `not_found` | Branch on the lookup *result*, not its error |\n| Conflicts with current state (duplicate, race) | 409 | `conflict` | Detect with logic |\n| Caller exceeded rate limit | 429 | `rate_limit_exceeded` | Set `Retry-After` header |\n| Node threw, cause unknown | 500 | `internal_error` | Error output path |\n| Third-party API returned an error | 502 | `upstream_error` | Error output of the HTTP node |\n| Can't process right now (downstream down) | 503 | `service_unavailable` | Detect specific error, hint retry |\n| Third-party API timed out | 504 | `upstream_timeout` | Error output filtered by message |\n\nSo there are two distinct flows: **4xx is decided before the work** (IF/Switch + dedicated Respond), **5xx comes out of error outputs** (\"we tried, it broke\").\n\n**One Respond, expression-driven code.** When error paths differ only by *number and message* (same body shape, same headers), don't fan out to N Respond nodes through a Switch. The Respond node accepts expressions in both `Response Code` and body — compute the code inline:\n\n```javascript\n// Response Code field on a single Respond to Webhook:\n{{ (() => {\n    const msg = $json.error?.message || $json.message || '';\n    if (msg.includes('INVALID_ID')) return 400;\n    if (/429|too many/i.test(msg)) return 429;\n    if (/timeout/i.test(msg))      return 504;\n    if (/upstream|llm|api/i.test(msg)) return 502;\n    return 500;\n})() }}\n```\n\nReserve Switch + multiple Responds for paths that diverge *structurally* (different headers, different body shapes, redirects). Same shape with a different number is one expression-driven Respond.\n\nThe default envelope is `{ \"error\": \"<code>\", \"message\": \"<human text>\" }` — the HTTP status already says success-vs-failure, so no `ok: false` flag. **Never leak internals** (stack traces, SQL, upstream bodies, tokens) into the response — log those privately, return a sanitized message. Correlation IDs, `retry_after`, validation `details`, and the full do-not-leak list are in **RESPONSE_SHAPES.md**.\n\n---\n\n## Workflow-level error workflow (the catch-all)\n\nPer-node outputs handle the failures you anticipated on the nodes you remembered to wire. An **error workflow** catches everything else: a node you forgot to wire, a crash between nodes, a whole-workflow timeout, a trigger failure. For unattended workflows this is the safety net that turns \"it silently stopped\" into \"an alert arrived\".\n\nBuild it as a separate workflow starting with an **Error Trigger** node. n8n invokes it with the failure context:\n\n```json\n{\n  \"execution\": { \"id\": \"...\", \"url\": \"...\", \"lastNodeExecuted\": \"Fetch order\",\n    \"error\": { \"name\": \"NodeApiError\", \"message\": \"...\", \"timestamp\": 1715000000000 } },\n  \"workflow\": { \"id\": \"...\", \"name\": \"Sync Stripe customers\" }\n}\n```\n\nMinimal version — **capture → notify**:\n\n```\nError Trigger → Set (build alert from execution + error) → Slack/email (post to #incidents)\n```\n\nA good alert includes the workflow name, a link to the editor and a link to the failed execution, the failed node name, and the **real** error message (not \"Workflow failed\"). Field expressions and the optional \"fetch the failing input via the n8n node\" upgrade are in **ERROR_WORKFLOWS.md**.\n\nTwo traps worth flagging up front:\n\n- **The recursion trap.** If the error workflow notifies Slack and Slack is what's down, the error workflow fails too — and the original error vanishes. Notify on a *different* channel than your monitored workflows use (most workflows alert Slack → error workflow uses email), and add a fallback (write to a Data Table) so a failed notification still leaves a trace.\n- **A \"handled\" error won't bubble up.** If a node's error output is wired to a no-op that drops the data, n8n considers the error *handled* and the error workflow does **not** fire. Only catch per-node when you're actually doing something with the error.\n\n> **What the community MCP can't do:** assigning the error workflow (instance default or per-workflow override) is an n8n **UI setting** — Workflow Settings → Error Workflow. There is no MCP tool to set it. Build the error workflow with the MCP, then tell the user the exact UI step to wire it up, and to repeat it (or set the instance default) for every unattended workflow.\n\n---\n\n## What's NOT available via the community MCP\n\n| Want to do | Reality |\n|---|---|\n| Set a workflow's **Error Workflow** setting | UI only (Workflow Settings → Error Workflow). No MCP tool. Build the workflow, then hand the user the UI step. |\n| Toggle other **workflow settings** (Save Execution Data, timezone, timeout, caller policy) | UI only. `n8n_update_partial_workflow` has `updateSettings`, but the error-workflow assignment is not reliably exposed — confirm in the UI. |\n| Enable instance-wide error logging (Sentry, server logs) | Instance config, outside n8n workflows entirely. |\n\nWhat the MCP **can** do: build the error workflow, set `onError`/`retryOnFail` on nodes (`updateNode`/`patchNodeField`), wire error outputs (`addConnection` with `sourceIndex: 1`), validate (`validate_workflow`, `n8n_validate_workflow`), auto-fix common issues (`n8n_autofix_workflow`), test (`n8n_test_workflow`), and inspect failures (`n8n_executions`).\n\n---\n\n## Anti-patterns\n\n| Anti-pattern | What goes wrong | Fix |\n|---|---|---|\n| `onError` set but error output unwired | Error silently discarded; run shows as **succeeded** | Wire `sourceIndex: 1` to a real handler, or revert `onError` to `stopWorkflow` so it's loud |\n| Error output wired but `onError` not set | Slot never fires; handler unreachable; workflow halts on failure | Set `onError: \"continueErrorOutput\"` |\n| Webhook → process → respond, no error branch | Caller gets a timeout or n8n's generic 500 | Wire every fallible node's error output to a Respond |\n| Error branch returns 200 with an `{error}` body | Caller's client reads success; their error handling never fires | Set `responseCode` to 4xx/5xx explicitly on error Responds |\n| One 500 `internal_error` for everything | Caller can't tell their bad input from your outage | Map cause → status (4xx caller, 5xx you) |\n| Catching errors in a Code node and returning them as data | Downstream processes error-shaped data and continues | Let it throw; use `onError: \"continueErrorOutput\"` + wired path |\n| Network node with no `retryOnFail` | Every transient 429/blip surfaces as a 5xx; alerts fire on noise | `retryOnFail: true, maxTries: 3, waitBetweenTries: 5000` |\n| Switch → N Responds differing only by status code | 5 nodes for what's one Respond | Compute the code inline in one expression-driven Respond |\n| Unattended workflow with no error workflow | A genuine failure goes nowhere | Build an Error Trigger workflow + assign it in the UI |\n| Error workflow notifies the same channel the workflows monitor | Channel down → error workflow also fails → error vanishes | Use a different channel + a Data Table fallback |\n| Leaking `$json.error` (stack/SQL/tokens) into the response | Exposes internals to callers/attackers | Log privately, return a sanitized message |\n\n---\n\n## Reference files\n\n| File | Read when |\n|---|---|\n| **NODE_ERROR_OUTPUTS.md** | Wiring a per-node error output on individual fallible nodes |\n| **API_WORKFLOWS.md** | Building/reviewing a webhook → Respond workflow, including the schema validator |\n| **RESPONSE_SHAPES.md** | Defining response body conventions, status codes, and what not to leak |\n| **ERROR_WORKFLOWS.md** | Setting up the workflow-level catch-all for unattended workflows |\n\n---\n\n## Integration with other skills\n\n- **n8n-workflow-patterns** — the webhook/API and scheduled patterns are where error handling lives. Use it for the overall shape; use this skill to harden it.\n- **n8n-node-configuration** — `onError`/`retryOnFail` are node config; NODE_FAMILY_GOTCHAS.md covers the Webhook/Respond response-code traps in depth.\n- **n8n-validation-expert** — the half-wired error output (one of the two steps missing) is a connection/config audit item, not a validation error. This skill is the fix.\n- **n8n-expression-syntax** — the expression-driven `Response Code` and the alert-message expressions rely on correct `{{ }}` syntax and `$json.error` access.\n- **n8n-code-javascript / n8n-code-python** — if you catch errors *inside* a Code node, decide deliberately: re-throw to use the error output, or handle and continue. Don't return error-shaped data and pretend it succeeded.\n- **n8n-code-tool** — an agent's Code Tool surfaces thrown errors back to the LLM, which then retries; that's a different error contract from workflow nodes.\n- **n8n-binary-and-data** — file/binary operations are fallible too; wire their error outputs like any network node.\n\n---\n\n## Quick reference checklist\n\nFor an **API / webhook** workflow:\n\n- [ ] Webhook trigger uses `responseMode: \"responseNode\"`\n- [ ] Input validated upstream → 4xx Respond (schema validator or IF)\n- [ ] Every fallible node has `onError: \"continueErrorOutput\"` **and** `main[1]` wired\n- [ ] Network nodes have `retryOnFail: true, maxTries: 3, waitBetweenTries: 5000`\n- [ ] Error path ends at a Respond with an **explicit** 4xx/5xx `responseCode`\n- [ ] Status code matches cause (4xx caller, 5xx you)\n- [ ] Error body is `{ error, message }` — no stack traces, SQL, or tokens\n- [ ] Verified with `n8n_get_workflow`: both `onError` and `main[1]` present on each fallible node\n\nFor an **unattended** (scheduled/cron/queue) workflow:\n\n- [ ] Network nodes have `retryOnFail` configured\n- [ ] An Error Trigger workflow exists (capture → notify, optional retry)\n- [ ] The error workflow notifies on a different channel + has a fallback (recursion trap)\n- [ ] The error-workflow setting is assigned in the n8n UI (MCP can't do it — remind the user)\n\n---\n\n**Remember**: the default is silence. Error handling is two moves — make the failure *route* (per-node `onError` + wired output, or a catch-all error workflow) and make it *speak* (a status code and body that tell the truth). Half a move is worse than none, because it looks done.\n","data/skills/n8n-self-hosting/SKILL.md":"---\nname: n8n-self-hosting\ndescription: Deploy a production self-hosted n8n end-to-end to a fresh Linux VM over SSH, using Docker Compose behind a Caddy reverse proxy with automatic HTTPS. Use whenever the user wants to self-host, install, set up, provision, or deploy n8n on their own server/VPS/box (Hetzner, DigitalOcean, AWS EC2, bare metal, etc.) — in either single/regular mode or queue mode with workers — or to update, back up, restore, or harden such an instance. This is for SELF-HOSTED n8n (Docker), not n8n Cloud and not building workflows. The skill makes the agent ask single-vs-queue first, collect the domain/SSH/timezone inputs, generate fresh secrets on the box, and bring the stack up with TLS. Trigger on \"deploy n8n\", \"self-host n8n\", \"install n8n on my server\", \"n8n docker compose\", \"n8n queue mode / workers / scaling\", \"n8n reverse proxy / SSL\", \"back up / update my n8n\", or \"we don't want to give every user the OAuth client secret\" / \"enable the Sign in with Google button\" (credential overwrites).\n---\n\n# Deploying self-hosted n8n\n\nThis skill takes a **fresh Linux VM** (Ubuntu/Debian, root or sudo SSH) to a **running,\nHTTPS, production n8n** via Docker Compose behind **Caddy** (automatic Let's Encrypt TLS).\nIt is for **self-hosted n8n on Docker** — not n8n Cloud, and not for building workflows\n(that's the rest of this pack).\n\nTwo deployment modes. The architectures differ, so **pick the mode before doing anything**.\n\nYou drive this end-to-end over SSH: preflight → install Docker → lay down the project →\ngenerate secrets → launch → verify TLS → hand off. The template files live in `assets/`;\nthe per-mode and security depth live in the reference files named below.\n\n## Rule 0 — choose the mode (ask the user)\n\nDo not guess. Ask, then commit to one:\n\n| | **Single / regular** | **Queue** |\n|---|---|---|\n| Processes | one n8n | main + N workers |\n| Extra services | none (SQLite) | Redis (queue) + Postgres (DB) |\n| Executes workflows | in the main process | on workers, in parallel |\n| Good for | 1 user, light/moderate load, simplest ops | high volume, heavy/long executions, horizontal scale |\n| Compose | `assets/docker-compose.single.yml` | `assets/docker-compose.queue.yml` |\n| Deep dive | **`SINGLE_MODE.md`** | **`QUEUE_MODE.md`** |\n\nIf unsure, start **single** — it's the simplest correct thing and covers most needs. Moving\nto queue later means swapping the compose file and migrating SQLite→Postgres, so if the user\nalready expects real volume, start **queue**.\n\n## Rule 1 — secret hygiene (non-negotiable)\n\nA misstep here leaks client credentials. Be diligent:\n\n1. **Generate every secret fresh, on the target box.** Never copy an encryption key, DB\n   password, or `.env` from another n8n instance into this one. See `SECURITY.md` for the\n   `openssl` commands.\n2. **Secrets live only in `.env`** (mode 600), referenced by the compose as `${VAR}`. Never\n   inline a secret into `docker-compose.yml`, the Caddyfile, or anything you commit.\n3. **The `N8N_ENCRYPTION_KEY` is sacred.** It encrypts every stored credential. If it's lost\n   or changes, all saved credentials become undecryptable. Set it explicitly, and tell the\n   user to back it up **off the box**. Don't echo it into long-lived logs or chat history\n   beyond what's needed to hand it over.\n4. **Never expose internal services.** Only Caddy (80/443) is public. n8n (5678), Postgres\n   (5432), Redis (6379) stay on the private Docker network — the templates already omit their\n   host port mappings. Don't add them.\n5. **`.env` and Caddy's `caddy_data` volume (the issued certs + ACME account key) are not\n   artifacts to share.** If you're working inside a git repo, confirm `.env` is git-ignored\n   before any commit.\n\n## Inputs to collect up front\n\n- **SSH target** — `user@host` and how you authenticate (key path or the user confirms the agent already has access). Root or a sudo user.\n- **Domain** — the full hostname n8n will live at, e.g. `n8n.example.com` (→ `SUBDOMAIN=n8n`, `DOMAIN_NAME=example.com`). The user must control its DNS.\n- **TLS email** — for Let's Encrypt (`SSL_EMAIL`).\n- **Timezone** — IANA name for Schedule/Cron nodes (e.g. `Europe/Warsaw`), else `Etc/UTC`.\n- **Mode** — single or queue (Rule 0). Queue → confirm the box has enough RAM (rough floor ~4 GB; each worker wants ~1–2 GB).\n- **Optional modules** — some features (currently **Agents**) are backend modules that stay off\n  unless listed in `N8N_ENABLED_MODULES`. Ask only if the user brings one up; if they do, read\n  the modules section of `QUEUE_MODE.md` before enabling it, because in queue mode it has to\n  reach the workers as well.\n\n## The deploy flow\n\nWork through these in order. `SINGLE_MODE.md` / `QUEUE_MODE.md` give the mode-specific command\ndetail; `SECURITY.md` covers secret generation and hardening; `DAY2.md` covers update/backup/restore.\n\n### 1. Preflight (the cheapest failure is the one you catch here)\n- SSH in; confirm the OS is Debian/Ubuntu-like (`. /etc/os-release`).\n- **DNS must already point at the box.** Compare the box's public IP (`curl -s ifconfig.me`)\n  with `dig +short <fqdn>` (run it from the box AND ideally your laptop). If they don't match,\n  **stop** — Caddy's ACME challenge will fail. Have the user create the A record, wait for it\n  to propagate, then continue.\n- Ports **80 and 443** must be reachable from the internet. Check the host firewall AND any\n  cloud security group / network firewall (Hetzner Cloud, AWS SG, etc.) — these are outside\n  the box and a common silent blocker.\n\n### 2. Install Docker (if absent)\n- Check `docker --version` and `docker compose version`. If missing, install Docker Engine +\n  the Compose plugin (Docker's official `get.docker.com` script on Ubuntu/Debian is fine).\n  Re-check `docker compose version` before proceeding.\n\n### 3. Lay down the project\n- Pick `DATA_FOLDER` — an **absolute path**, e.g. `/opt/n8n`. The `DATA_FOLDER` value in `.env`\n  **must equal this exact directory** (the compose mounts `${DATA_FOLDER}/caddy_config/Caddyfile`,\n  and `init-data.sh` is mounted via a relative `./` path), so always run `docker compose` from\n  here. Create it, plus `caddy_config/` and `local_files/` inside.\n- **Get the template files onto the box.** They live in this skill's `assets/` on *your* machine,\n  not on the server — transfer each one. Either `scp` them up, or (no local copy needed) write\n  each file's contents over SSH, e.g.\n  `ssh <target> 'cat > <DATA_FOLDER>/docker-compose.yml' < assets/docker-compose.single.yml`.\n  Land them with these exact names:\n  - the chosen compose → `<DATA_FOLDER>/docker-compose.yml` (rename it to exactly this)\n  - `Caddyfile` → `<DATA_FOLDER>/caddy_config/Caddyfile`\n  - **queue only:** `init-data.sh` → `<DATA_FOLDER>/init-data.sh`, then `chmod +x` it\n  - the matching `.env.*.example` → `<DATA_FOLDER>/.env`\n\n### 4. Fill `.env` + generate secrets\n- Set `DATA_FOLDER`, `DOMAIN_NAME`, `SUBDOMAIN`, `SSL_EMAIL`, `GENERIC_TIMEZONE`.\n- Generate each secret **on the box** with `openssl` (`SECURITY.md` has the commands) and **write\n  it into `.env`, replacing the matching `REPLACE_WITH_…` placeholder**: `N8N_ENCRYPTION_KEY`;\n  queue also `POSTGRES_PASSWORD` + `POSTGRES_NON_ROOT_PASSWORD`.\n- **Before launching, confirm none are left unset:** `grep REPLACE_WITH_ .env` must return nothing\n  — a leftover placeholder becomes the literal password and Postgres/n8n fail to connect.\n- `chmod 600 .env`. Record the encryption key so the user can back it up off-box.\n\n### 5. Firewall\n- `ufw`: allow OpenSSH + 80 + 443, then enable. Do **not** open 5678/5432/6379.\n\n### 6. Launch\n- `cd <DATA_FOLDER> && docker compose up -d`.\n- Queue mode brings up Redis + Postgres + main + workers (workers via `replicas`). To add\n  capacity: `docker compose up -d --scale n8n-worker=N`.\n\n### 7. Verify (don't declare success without this)\n- `docker compose ps` — every service `Up`/healthy (queue: postgres & redis `healthy` first).\n- **n8n itself up (internal):** `docker compose exec n8n wget -qO- http://localhost:5678/healthz`\n  → `{\"status\":\"ok\"}`. This separates \"n8n is running\" from \"TLS isn't ready yet.\"\n- **Cert issued:** `docker compose logs caddy | grep -i 'certificate obtained'`. First-boot ACME\n  can take a minute or two; until it finishes, a public `https://` request fails TLS — that means\n  the cert is still pending, **not** that n8n is down.\n- **Public reachability (with retry):** `curl -fsS --retry 5 --retry-delay 10 https://<fqdn>/healthz`\n  → `{\"status\":\"ok\"}`. (`/healthz` only proves the process is reachable; `/healthz/readiness`\n  additionally confirms the DB is connected and migrated — use it when debugging a boot loop.)\n- **Queue mode — main and workers must agree.** Diff their environments:\n  `diff <(docker compose exec -T n8n env | sort) <(docker compose exec -T --index 1 n8n-worker env | sort)`.\n  Only the public-URL/proxy vars should differ. Anything else means a behavioural setting reached\n  the main but not the workers — and workers are what execute workflows, so it fails at runtime\n  in one node rather than at boot. `QUEUE_MODE.md` explains the rule.\n- Open `https://<fqdn>` → the **owner setup** screen. **Whoever completes that signup form first\n  claims the instance** — an exposed un-owned instance is a race, so create the owner account\n  immediately, before sharing the URL. Enable 2FA. (Automated deploys can pre-provision the\n  owner via env vars instead — see the owner row in `SECURITY.md`.)\n\n### 8. Hand off\n- Give the user: the URL, where the project lives, the encryption key to store safely, and the\n  Day-2 basics (update / backup / restore) from **`DAY2.md`**.\n\n## What NOT to do\n\n- **Don't skip the DNS/ports preflight.** A wrong A record or a closed cloud firewall is the\n  #1 reason Caddy can't get a cert and n8n looks \"broken.\"\n- **Don't publish 5678/5432/6379** to the host. Caddy reaches n8n over the private network.\n- **Don't reuse another instance's encryption key or `.env`.** Fresh secrets per box.\n- **Don't run queue mode on SQLite.** Queue requires Postgres (the template already wires it).\n- **Don't put secrets in `docker-compose.yml` or the Caddyfile.** `.env` only.\n- **Don't add a behavioural env var to the main only (queue mode).** Modules, DB, queue,\n  binary-data and encryption settings belong in the shared `x-n8n-env` anchor so workers get\n  them too; only the public-URL/proxy vars are main-only. See `QUEUE_MODE.md`.\n- **Don't use `:latest` blindly.** Pin `N8N_IMAGE_TAG`; update deliberately (`DAY2.md`).\n\n## Reference files\n\n- **`SINGLE_MODE.md`** — single-instance specifics, SQLite vs Postgres, when to graduate to queue.\n- **`QUEUE_MODE.md`** — queue architecture, workers/concurrency/scaling, shared encryption key, the main-vs-worker **env-parity rule**, optional backend modules (`N8N_ENABLED_MODULES`, e.g. Agents), binary data (`database` mode — filesystem is unsupported in queue mode; S3/Azure = Enterprise), webhook processors, multi-main licensing.\n- **`SECURITY.md`** — generating secrets, the encryption-key rules, the full hardening checklist (telemetry off, env-access block, public API, firewall, secure cookies).\n- **`CREDENTIAL_OVERWRITES.md`** — managed OAuth: register one OAuth app instance-wide so users\n  never see a client ID/secret (\"Sign in with Google\" on self-hosted). The endpoint-vs-env choice,\n  the **mandatory** endpoint auth token, parent-type inheritance, persistence and worker reload.\n- **`DAY2.md`** — changing a setting (env var) safely, updating the image, backing up (encryption key + volume + Postgres), and restoring.\n- **`assets/`** — the templates: `docker-compose.single.yml`, `docker-compose.queue.yml`, `Caddyfile`, `.env.single.example`, `.env.queue.example`, `init-data.sh`.\n\nAuthoritative upstream reference: the official hosting docs live at\n<https://docs.n8n.io/deploy/host-n8n> (restructured mid-2026 from the old `/hosting/` paths —\nprefer these URLs). The env-var reference index is at\n<https://docs.n8n.io/deploy/host-n8n/configure-n8n/basic-configuration/use-environment-variables>.\nWhen this skill and the live docs disagree, trust the docs and tell the user.\n","data/skills/n8n-validation-expert/SKILL.md":"# n8n-validation-expert\n\nPath: `data/skills/n8n-validation-expert/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-validation-expert/SKILL.md)","data/skills/n8n-workflow-patterns/SKILL.md":"# n8n-workflow-patterns\n\nPath: `data/skills/n8n-workflow-patterns/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-workflow-patterns/SKILL.md)","data/skills/using-n8n-mcp-skills/SKILL.md":"# using-n8n-mcp-skills\n\nPath: `data/skills/using-n8n-mcp-skills/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/using-n8n-mcp-skills/SKILL.md)","AGENTS.md":"# AI Agent Protocol & Instructions\n\nPath: `AGENTS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/AGENTS.md)","CLAUDE.md":"# Claude Agent Guidelines & System Prompt\n\nPath: `CLAUDE.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/CLAUDE.md)",".claude/agents/code-reviewer.md":"# Subagent: code-reviewer\n\nPath: `.claude/agents/code-reviewer.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/.claude/agents/code-reviewer.md)",".claude/agents/context-manager.md":"# Subagent: context-manager\n\nPath: `.claude/agents/context-manager.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/.claude/agents/context-manager.md)",".claude/agents/debugger.md":"# Subagent: debugger\n\nPath: `.claude/agents/debugger.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/.claude/agents/debugger.md)",".claude/agents/deployment-engineer.md":"# Subagent: deployment-engineer\n\nPath: `.claude/agents/deployment-engineer.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/.claude/agents/deployment-engineer.md)",".claude/agents/mcp-backend-engineer.md":"# Subagent: mcp-backend-engineer\n\nPath: `.claude/agents/mcp-backend-engineer.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/.claude/agents/mcp-backend-engineer.md)",".claude/agents/n8n-mcp-tester.md":"# Subagent: n8n-mcp-tester\n\nPath: `.claude/agents/n8n-mcp-tester.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/.claude/agents/n8n-mcp-tester.md)",".claude/agents/technical-researcher.md":"# Subagent: technical-researcher\n\nPath: `.claude/agents/technical-researcher.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/.claude/agents/technical-researcher.md)",".claude/agents/test-automator.md":"# Subagent: test-automator\n\nPath: `.claude/agents/test-automator.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/.claude/agents/test-automator.md)","data/skills/n8n-agents/CHAT_AGENT_PATTERNS.md":"# N8n-agents Skill\n\nPath: `data/skills/n8n-agents/CHAT_AGENT_PATTERNS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-agents/CHAT_AGENT_PATTERNS.md)","data/skills/n8n-agents/EXAMPLES.md":"# N8n-agents Skill\n\nPath: `data/skills/n8n-agents/EXAMPLES.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-agents/EXAMPLES.md)","data/skills/n8n-agents/HUMAN_REVIEW.md":"# N8n-agents Skill\n\nPath: `data/skills/n8n-agents/HUMAN_REVIEW.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-agents/HUMAN_REVIEW.md)","data/skills/n8n-agents/MEMORY.md":"# N8n-agents Skill\n\nPath: `data/skills/n8n-agents/MEMORY.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-agents/MEMORY.md)","data/skills/n8n-agents/RAG.md":"# N8n-agents Skill\n\nPath: `data/skills/n8n-agents/RAG.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-agents/RAG.md)","data/skills/n8n-agents/README.md":"# N8n-agents Skill\n\nPath: `data/skills/n8n-agents/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-agents/README.md)","data/skills/n8n-agents/STRUCTURED_OUTPUT.md":"# N8n-agents Skill\n\nPath: `data/skills/n8n-agents/STRUCTURED_OUTPUT.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-agents/STRUCTURED_OUTPUT.md)","data/skills/n8n-agents/SUBWORKFLOW_AS_TOOL.md":"# N8n-agents Skill\n\nPath: `data/skills/n8n-agents/SUBWORKFLOW_AS_TOOL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-agents/SUBWORKFLOW_AS_TOOL.md)","data/skills/n8n-agents/SYSTEM_PROMPT.md":"# N8n-agents Skill\n\nPath: `data/skills/n8n-agents/SYSTEM_PROMPT.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-agents/SYSTEM_PROMPT.md)","data/skills/n8n-agents/TOOLS.md":"# N8n-agents Skill\n\nPath: `data/skills/n8n-agents/TOOLS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-agents/TOOLS.md)","data/skills/n8n-binary-and-data/AGENT_TOOL_BINARY.md":"# N8n-binary-and-data Skill\n\nPath: `data/skills/n8n-binary-and-data/AGENT_TOOL_BINARY.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-binary-and-data/AGENT_TOOL_BINARY.md)","data/skills/n8n-binary-and-data/BINARY_BASICS.md":"# N8n-binary-and-data Skill\n\nPath: `data/skills/n8n-binary-and-data/BINARY_BASICS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-binary-and-data/BINARY_BASICS.md)","data/skills/n8n-binary-and-data/CDN_REQUIREMENT.md":"# N8n-binary-and-data Skill\n\nPath: `data/skills/n8n-binary-and-data/CDN_REQUIREMENT.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-binary-and-data/CDN_REQUIREMENT.md)","data/skills/n8n-binary-and-data/MERGE_FOR_CONTEXT.md":"# N8n-binary-and-data Skill\n\nPath: `data/skills/n8n-binary-and-data/MERGE_FOR_CONTEXT.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-binary-and-data/MERGE_FOR_CONTEXT.md)","data/skills/n8n-binary-and-data/README.md":"# N8n-binary-and-data Skill\n\nPath: `data/skills/n8n-binary-and-data/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-binary-and-data/README.md)","data/skills/n8n-code-javascript/BUILTIN_FUNCTIONS.md":"# N8n-code-javascript Skill\n\nPath: `data/skills/n8n-code-javascript/BUILTIN_FUNCTIONS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-code-javascript/BUILTIN_FUNCTIONS.md)","data/skills/n8n-code-javascript/COMMON_PATTERNS.md":"# N8n-code-javascript Skill\n\nPath: `data/skills/n8n-code-javascript/COMMON_PATTERNS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-code-javascript/COMMON_PATTERNS.md)","data/skills/n8n-code-javascript/DATA_ACCESS.md":"# N8n-code-javascript Skill\n\nPath: `data/skills/n8n-code-javascript/DATA_ACCESS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-code-javascript/DATA_ACCESS.md)","data/skills/n8n-code-javascript/ERROR_PATTERNS.md":"# N8n-code-javascript Skill\n\nPath: `data/skills/n8n-code-javascript/ERROR_PATTERNS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-code-javascript/ERROR_PATTERNS.md)","data/skills/n8n-code-javascript/README.md":"# N8n-code-javascript Skill\n\nPath: `data/skills/n8n-code-javascript/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-code-javascript/README.md)","data/skills/n8n-code-python/COMMON_PATTERNS.md":"# N8n-code-python Skill\n\nPath: `data/skills/n8n-code-python/COMMON_PATTERNS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-code-python/COMMON_PATTERNS.md)","data/skills/n8n-code-python/DATA_ACCESS.md":"# N8n-code-python Skill\n\nPath: `data/skills/n8n-code-python/DATA_ACCESS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-code-python/DATA_ACCESS.md)","data/skills/n8n-code-python/ERROR_PATTERNS.md":"# N8n-code-python Skill\n\nPath: `data/skills/n8n-code-python/ERROR_PATTERNS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-code-python/ERROR_PATTERNS.md)","data/skills/n8n-code-python/README.md":"# N8n-code-python Skill\n\nPath: `data/skills/n8n-code-python/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-code-python/README.md)","data/skills/n8n-code-python/STANDARD_LIBRARY.md":"# N8n-code-python Skill\n\nPath: `data/skills/n8n-code-python/STANDARD_LIBRARY.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-code-python/STANDARD_LIBRARY.md)","data/skills/n8n-code-tool/ERROR_PATTERNS.md":"# N8n-code-tool Skill\n\nPath: `data/skills/n8n-code-tool/ERROR_PATTERNS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-code-tool/ERROR_PATTERNS.md)","data/skills/n8n-code-tool/INPUT_SCHEMA.md":"# N8n-code-tool Skill\n\nPath: `data/skills/n8n-code-tool/INPUT_SCHEMA.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-code-tool/INPUT_SCHEMA.md)","data/skills/n8n-code-tool/README.md":"# N8n-code-tool Skill\n\nPath: `data/skills/n8n-code-tool/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-code-tool/README.md)","data/skills/n8n-error-handling/API_WORKFLOWS.md":"# N8n-error-handling Skill\n\nPath: `data/skills/n8n-error-handling/API_WORKFLOWS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-error-handling/API_WORKFLOWS.md)","data/skills/n8n-error-handling/ERROR_WORKFLOWS.md":"# N8n-error-handling Skill\n\nPath: `data/skills/n8n-error-handling/ERROR_WORKFLOWS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-error-handling/ERROR_WORKFLOWS.md)","data/skills/n8n-error-handling/NODE_ERROR_OUTPUTS.md":"# N8n-error-handling Skill\n\nPath: `data/skills/n8n-error-handling/NODE_ERROR_OUTPUTS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-error-handling/NODE_ERROR_OUTPUTS.md)","data/skills/n8n-error-handling/README.md":"# N8n-error-handling Skill\n\nPath: `data/skills/n8n-error-handling/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-error-handling/README.md)","data/skills/n8n-error-handling/RESPONSE_SHAPES.md":"# N8n-error-handling Skill\n\nPath: `data/skills/n8n-error-handling/RESPONSE_SHAPES.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-error-handling/RESPONSE_SHAPES.md)","data/skills/n8n-expression-syntax/COMMON_MISTAKES.md":"# N8n-expression-syntax Skill\n\nPath: `data/skills/n8n-expression-syntax/COMMON_MISTAKES.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-expression-syntax/COMMON_MISTAKES.md)","data/skills/n8n-expression-syntax/EXAMPLES.md":"# N8n-expression-syntax Skill\n\nPath: `data/skills/n8n-expression-syntax/EXAMPLES.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-expression-syntax/EXAMPLES.md)","data/skills/n8n-expression-syntax/README.md":"# N8n-expression-syntax Skill\n\nPath: `data/skills/n8n-expression-syntax/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-expression-syntax/README.md)","data/skills/n8n-mcp-tools-expert/OPERATIONS_GUIDE.md":"# N8n-mcp-tools-expert Skill\n\nPath: `data/skills/n8n-mcp-tools-expert/OPERATIONS_GUIDE.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-mcp-tools-expert/OPERATIONS_GUIDE.md)","data/skills/n8n-mcp-tools-expert/README.md":"# N8n-mcp-tools-expert Skill\n\nPath: `data/skills/n8n-mcp-tools-expert/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-mcp-tools-expert/README.md)","data/skills/n8n-mcp-tools-expert/SEARCH_GUIDE.md":"# N8n-mcp-tools-expert Skill\n\nPath: `data/skills/n8n-mcp-tools-expert/SEARCH_GUIDE.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-mcp-tools-expert/SEARCH_GUIDE.md)","data/skills/n8n-mcp-tools-expert/VALIDATION_GUIDE.md":"# N8n-mcp-tools-expert Skill\n\nPath: `data/skills/n8n-mcp-tools-expert/VALIDATION_GUIDE.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-mcp-tools-expert/VALIDATION_GUIDE.md)","data/skills/n8n-mcp-tools-expert/WORKFLOW_GUIDE.md":"# N8n-mcp-tools-expert Skill\n\nPath: `data/skills/n8n-mcp-tools-expert/WORKFLOW_GUIDE.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-mcp-tools-expert/WORKFLOW_GUIDE.md)","data/skills/n8n-multi-instance/README.md":"# N8n-multi-instance Skill\n\nPath: `data/skills/n8n-multi-instance/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-multi-instance/README.md)","data/skills/n8n-node-configuration/DEPENDENCIES.md":"# N8n-node-configuration Skill\n\nPath: `data/skills/n8n-node-configuration/DEPENDENCIES.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-node-configuration/DEPENDENCIES.md)","data/skills/n8n-node-configuration/NODE_FAMILY_GOTCHAS.md":"# N8n-node-configuration Skill\n\nPath: `data/skills/n8n-node-configuration/NODE_FAMILY_GOTCHAS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-node-configuration/NODE_FAMILY_GOTCHAS.md)","data/skills/n8n-node-configuration/OPERATION_PATTERNS.md":"# N8n-node-configuration Skill\n\nPath: `data/skills/n8n-node-configuration/OPERATION_PATTERNS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-node-configuration/OPERATION_PATTERNS.md)","data/skills/n8n-node-configuration/README.md":"# N8n-node-configuration Skill\n\nPath: `data/skills/n8n-node-configuration/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-node-configuration/README.md)","data/skills/n8n-self-hosting/CREDENTIAL_OVERWRITES.md":"# N8n-self-hosting Skill\n\nPath: `data/skills/n8n-self-hosting/CREDENTIAL_OVERWRITES.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-self-hosting/CREDENTIAL_OVERWRITES.md)","data/skills/n8n-self-hosting/DAY2.md":"# N8n-self-hosting Skill\n\nPath: `data/skills/n8n-self-hosting/DAY2.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-self-hosting/DAY2.md)","data/skills/n8n-self-hosting/QUEUE_MODE.md":"# N8n-self-hosting Skill\n\nPath: `data/skills/n8n-self-hosting/QUEUE_MODE.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-self-hosting/QUEUE_MODE.md)","data/skills/n8n-self-hosting/README.md":"# N8n-self-hosting Skill\n\nPath: `data/skills/n8n-self-hosting/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-self-hosting/README.md)","data/skills/n8n-self-hosting/SECURITY.md":"# N8n-self-hosting Skill\n\nPath: `data/skills/n8n-self-hosting/SECURITY.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-self-hosting/SECURITY.md)","data/skills/n8n-self-hosting/SINGLE_MODE.md":"# N8n-self-hosting Skill\n\nPath: `data/skills/n8n-self-hosting/SINGLE_MODE.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-self-hosting/SINGLE_MODE.md)","data/skills/n8n-subworkflows/NAMING_AND_DISCOVERY.md":"# N8n-subworkflows Skill\n\nPath: `data/skills/n8n-subworkflows/NAMING_AND_DISCOVERY.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-subworkflows/NAMING_AND_DISCOVERY.md)","data/skills/n8n-subworkflows/README.md":"# N8n-subworkflows Skill\n\nPath: `data/skills/n8n-subworkflows/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-subworkflows/README.md)","data/skills/n8n-subworkflows/SUBWORKFLOW_PATTERNS.md":"# N8n-subworkflows Skill\n\nPath: `data/skills/n8n-subworkflows/SUBWORKFLOW_PATTERNS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-subworkflows/SUBWORKFLOW_PATTERNS.md)","data/skills/n8n-validation-expert/ERROR_CATALOG.md":"# N8n-validation-expert Skill\n\nPath: `data/skills/n8n-validation-expert/ERROR_CATALOG.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-validation-expert/ERROR_CATALOG.md)","data/skills/n8n-validation-expert/FALSE_POSITIVES.md":"# N8n-validation-expert Skill\n\nPath: `data/skills/n8n-validation-expert/FALSE_POSITIVES.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-validation-expert/FALSE_POSITIVES.md)","data/skills/n8n-validation-expert/README.md":"# N8n-validation-expert Skill\n\nPath: `data/skills/n8n-validation-expert/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-validation-expert/README.md)","data/skills/n8n-validation-expert/REVIEW_CHECKLIST.md":"# N8n-validation-expert Skill\n\nPath: `data/skills/n8n-validation-expert/REVIEW_CHECKLIST.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-validation-expert/REVIEW_CHECKLIST.md)","data/skills/n8n-workflow-patterns/README.md":"# N8n-workflow-patterns Skill\n\nPath: `data/skills/n8n-workflow-patterns/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-workflow-patterns/README.md)","data/skills/n8n-workflow-patterns/ai_agent_workflow.md":"# N8n-workflow-patterns Skill\n\nPath: `data/skills/n8n-workflow-patterns/ai_agent_workflow.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-workflow-patterns/ai_agent_workflow.md)","data/skills/n8n-workflow-patterns/database_operations.md":"# N8n-workflow-patterns Skill\n\nPath: `data/skills/n8n-workflow-patterns/database_operations.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-workflow-patterns/database_operations.md)","data/skills/n8n-workflow-patterns/http_api_integration.md":"# N8n-workflow-patterns Skill\n\nPath: `data/skills/n8n-workflow-patterns/http_api_integration.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-workflow-patterns/http_api_integration.md)","data/skills/n8n-workflow-patterns/scheduled_tasks.md":"# N8n-workflow-patterns Skill\n\nPath: `data/skills/n8n-workflow-patterns/scheduled_tasks.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-workflow-patterns/scheduled_tasks.md)","data/skills/n8n-workflow-patterns/webhook_processing.md":"# N8n-workflow-patterns Skill\n\nPath: `data/skills/n8n-workflow-patterns/webhook_processing.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-workflow-patterns/webhook_processing.md)","data/skills/using-n8n-mcp-skills/README.md":"# Using-n8n-mcp-skills Skill\n\nPath: `data/skills/using-n8n-mcp-skills/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/using-n8n-mcp-skills/README.md)"},"items":[{"name":"AGENTS.md","path":"AGENTS.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/AGENTS.md","title":"AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# AI Agent Protocol & Instructions\n\nPath: `AGENTS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/AGENTS.md)","tokens":300,"sizeBytes":153},{"name":"CHAT_AGENT_PATTERNS.md","path":"data/skills/n8n-agents/CHAT_AGENT_PATTERNS.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-agents/CHAT_AGENT_PATTERNS.md","title":"N8n-agents Skill","category":"anthropic-skill","format":"markdown","content":"# N8n-agents Skill\n\nPath: `data/skills/n8n-agents/CHAT_AGENT_PATTERNS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-agents/CHAT_AGENT_PATTERNS.md)","tokens":300,"sizeBytes":209},{"name":"EXAMPLES.md","path":"data/skills/n8n-agents/EXAMPLES.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-agents/EXAMPLES.md","title":"N8n-agents Skill","category":"anthropic-skill","format":"markdown","content":"# N8n-agents Skill\n\nPath: `data/skills/n8n-agents/EXAMPLES.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-agents/EXAMPLES.md)","tokens":300,"sizeBytes":187},{"name":"HUMAN_REVIEW.md","path":"data/skills/n8n-agents/HUMAN_REVIEW.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-agents/HUMAN_REVIEW.md","title":"N8n-agents Skill","category":"anthropic-skill","format":"markdown","content":"# N8n-agents Skill\n\nPath: `data/skills/n8n-agents/HUMAN_REVIEW.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-agents/HUMAN_REVIEW.md)","tokens":300,"sizeBytes":195},{"name":"MEMORY.md","path":"data/skills/n8n-agents/MEMORY.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-agents/MEMORY.md","title":"N8n-agents Skill","category":"anthropic-skill","format":"markdown","content":"# N8n-agents Skill\n\nPath: `data/skills/n8n-agents/MEMORY.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-agents/MEMORY.md)","tokens":300,"sizeBytes":183},{"name":"RAG.md","path":"data/skills/n8n-agents/RAG.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-agents/RAG.md","title":"N8n-agents Skill","category":"anthropic-skill","format":"markdown","content":"# N8n-agents Skill\n\nPath: `data/skills/n8n-agents/RAG.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-agents/RAG.md)","tokens":300,"sizeBytes":177},{"name":"README.md","path":"data/skills/n8n-agents/README.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-agents/README.md","title":"N8n-agents Skill","category":"anthropic-skill","format":"markdown","content":"# N8n-agents Skill\n\nPath: `data/skills/n8n-agents/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-agents/README.md)","tokens":300,"sizeBytes":183},{"name":"SKILL.md","path":"data/skills/n8n-agents/SKILL.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-agents/SKILL.md","title":"n8n-agents","category":"anthropic-skill","format":"markdown","content":"---\nname: n8n-agents\ndescription: Design n8n AI agents the right way. Use when building or editing any @n8n/n8n-nodes-langchain.* AI node — an AI Agent, LLM chain, Text Classifier, or Information Extractor — and whenever the user mentions AI agents, LLM with tools, tool calling, $fromAI, system prompts, agent memory, sessionId, structured/JSON output, output parser, RAG, vector store, a chat assistant/bot, or human-in-the-loop review. Covers Agent-vs-chain-vs-classifier choice, the model/memory/tools/outputParser slots, tool names/descriptions as prompt, structured output with autoFix, memory, RAG, human review, and chat topologies.\n---\n\n# n8n Agents\n\nThe n8n AI Agent node (`@n8n/n8n-nodes-langchain.agent`) is a multi-turn LLM driver with sub-nodes for the model, memory, tools, and an optional output parser. This skill is the **deep** guide to designing agents and the LangChain family around them. For the high-level \"where an agent fits in a workflow\" picture, see **n8n-workflow-patterns** `ai_agent_workflow.md` — this skill goes one level down into *how to build it well*.\n\nFor node-type formats: in workflow JSON the LangChain nodes use the long `@n8n/n8n-nodes-langchain.*` form (`.agent`, `.lmChatOpenAi`, `.memoryBufferWindow`, `.outputParserStructured`, `.toolWorkflow`, `.toolHttpRequest`, `.toolCode`). When you call `get_node` / `validate_node`, use the **short** form (`nodes-langchain.agent`). See **n8n-mcp-tools-expert** for the format rules.\n\n---\n\n## Pick the right node first\n\nReaching for an Agent when the task is one-shot classification or extraction is the most common over-build. Decide before you wire anything:\n\n| You need to… | Use | Why |\n|---|---|---|\n| Call tools, reason over multiple turns, or hold memory | **AI Agent** (`.agent`) | The full loop: model + tools + memory + optional parser. Also a fine default when you'd rather standardize. |\n| One-shot text in → text out, no tools | **Basic LLM Chain** (`.chainLlm`) | No agent loop, easier to debug. Still accepts an `outputParserStructured` sub-node. |\n| Route a natural-language input to one of **N branches** | **Text Classifier** (`.textClassifier`) | ONE node, N output handles, downstream wires directly into each. Not Agent + Switch. |\n| Pull structured fields out of free text | **Information Extractor** (`.informationExtractor`) | Purpose-built field extraction with a schema. |\n| 3-way positive/neutral/negative split | **Sentiment Analysis** (`.sentimentAnalysis`) | Built-in branch outputs. |\n| Condense a long document | **Summarization Chain** (`.chainSummarization`) | Map-reduce summarization built in. |\n| Generate an image / audio / video | **The provider's native single-call node** (OpenAI, Gemini, ElevenLabs…) | NEVER wrap media generation in an Agent — see \"Binary and the agent boundary\". |\n\n**Text Classifier detail (the Agent + Switch anti-pattern):** every category needs both a **name AND a description**. The model routes against the *description*, not the name — a category with no description gets picked by coin-flip. Set `options.enableAutoFixing: true` for robustness on edge inputs. One node, N branches, done. Reaching for an Agent that \"decides\" then a Switch that \"routes\" is two nodes plus prompt boilerplate for what Text Classifier does natively.\n\nChat-model nodes (`.lmChatOpenAi`, `.lmChatAnthropic`, `.lmChatOpenRouter`, …) are **sub-nodes** — they don't run standalone. They wire into a chain, agent, classifier, or extractor via the `ai_languageModel` connection.\n\n---\n\n## The sub-node pattern\n\nThe Agent has a **main input** (the prompt / user message) and up to four **sub-node slots**, each wired by its own `ai_*` connection type:\n\n| Slot | Connection type | Required? | Node example |\n|---|---|---|---|\n| **model** | `ai_languageModel` | Yes | `.lmChatOpenAi`, `.lmChatAnthropic`, `.lmChatOpenRouter` |\n| **memory** | `ai_memory` | Optional | `.memoryBufferWindow`, `.memoryPostgresChat` |\n| **tools** | `ai_tool` | Optional (but the point of an agent) | `slackTool`, `.toolWorkflow`, `.toolHttpRequest`, `.toolCode` |\n| **outputParser** | `ai_outputParser` | Optional | `.outputParserStructured` |\n\nA sub-node connects FROM itself TO the agent. In workflow JSON the connection lives on the **sub-node**, keyed by the `ai_*` type:\n\n```json\n\"Main LLM\": {\n  \"ai_languageModel\": [[{ \"node\": \"AI Agent\", \"type\": \"ai_languageModel\", \"index\": 0 }]]\n},\n\"Simple Memory\": {\n  \"ai_memory\": [[{ \"node\": \"AI Agent\", \"type\": \"ai_memory\", \"index\": 0 }]]\n},\n\"Search customer DB\": {\n  \"ai_tool\": [[{ \"node\": \"AI Agent\", \"type\": \"ai_tool\", \"index\": 0 }]]\n}\n```\n\nMultiple tools all connect into the same `ai_tool` index 0 — they stack, they don't fan into separate indices. With `n8n_update_partial_workflow` you wire each with an `addConnection` op using `sourceOutput: \"ai_tool\"`. The agent puts its final answer in **`$json.output`** (not `.text`, not `.response`) — downstream nodes read `{{ $json.output }}`.\n\nSee **EXAMPLES.md** for a complete stateless agent-core node-object snippet.\n\n---\n\n## Two non-negotiables\n\n1. **Tool names and descriptions ARE part of the prompt.** The model picks a tool by reading its name and description — nothing else. A tool named `tool1` with an empty description is invisible to the model: it skips it, mis-selects it, or hallucinates parameters. There's usually no error — just an agent that \"won't use my tool\". Treat both like API design. → **TOOLS.md**\n2. **Structured output must parse AND autoFix.** An `outputParserStructured` with `autoFix: true` and a **coding-capable fixer model** is the production pattern. Without autoFix, one malformed JSON response halts the whole workflow. → **STRUCTURED_OUTPUT.md**\n\n---\n\n## Strong defaults\n\n- **Per-tool usage goes in the tool description, not the system prompt.** Anything about *how to call this specific tool* belongs with the tool, so it travels across agents and keeps the system prompt focused. → **SYSTEM_PROMPT.md**\n- **Sub-workflow tools (`.toolWorkflow`) for anything multi-step.** Any workflow becomes a tool with typed `$fromAI()` inputs, and composes with branching, error handling, and reuse. Default here when in doubt. → **SUBWORKFLOW_AS_TOOL.md** and **n8n-subworkflows**.\n- **Wrap tools with user-visible side effects in human review.** Sends, payments, refunds, account changes get gated behind an approval node so a human signs off before the tool fires. → **HUMAN_REVIEW.md**\n- **Raise `maxIterations`.** The default tool-call cap is **low** (single digits on most versions) — fine for a one-tool agent, far too low for a multi-tool agent that chains several calls per turn. It surfaces as \"max iterations reached\" or empty output. Set `options.maxIterations` to a realistic ceiling (15 for a focused sub-agent, 50-200 for a broad orchestrator).\n- **Put the current date in the system prompt** via `{{ $now }}` (or `{{ $now.format('DDDD') }}`). A hardcoded date is stale immediately.\n\n---\n\n## The four tool types\n\nPick the lightest option that covers the job:\n\n| Tool type | Node | Use when |\n|---|---|---|\n| **Native tool node** | `slackTool`, `gmailTool`, `toolCalculator`, … | The capability maps to one existing node + one operation. Lowest overhead. |\n| **Sub-workflow as tool** | `.toolWorkflow` | More than one node, reusable logic, or you want independent testability. The canonical n8n way — **default when in doubt**. |\n| **HTTP Request Tool** | `.toolHttpRequest` | A single external HTTP API the agent should orchestrate directly. Reuse the service's predefined credential to cover operations a native node doesn't expose. |\n| **MCP Client Tool** | `.mcpClientTool` | A maintained MCP server already covers it, or you want one published workflow to serve many agents. |\n\nThere is also a **Custom Code Tool** (`.toolCode`) for pure inline computation — but its runtime contract (string in / string out, no `$fromAI`, no `$helpers`) is owned by the **n8n-code-tool** skill. Read that before writing one. Rule of thumb: if you find yourself reaching for `$fromAI()` inside the code, you want `.toolWorkflow` instead.\n\n### `$fromAI()`: how the agent fills tool parameters\n\nTool parameters the agent should decide are wrapped in `$fromAI()`. It is a **real n8n expression helper**, used inside a tool node's parameter expressions:\n\n```\n={{ $fromAI('paramName', 'what to put here — be specific: format, range, example', 'string') }}\n```\n\n- **paramName** — the name the model uses internally (snake_case or camelCase, be consistent).\n- **description** — tells the model what value to produce. **It is part of the prompt** — write it like JSDoc.\n- **type** (optional) — `'string'` (default), `'number'`, `'boolean'`, `'json'`. A wrong-typed value fails the call.\n- **defaultValue** (optional) — used when the model omits it.\n\n`$fromAI()` carries JSON only — it **cannot carry binary** (no base64, no file bytes). And not every parameter has to be `$fromAI`: plumb identity, authority limits, and correlation IDs (`userId`, refund caps, `sessionId`) deterministically from workflow context so the agent can't get them wrong or even see them. → **TOOLS.md** for the full anatomy and the \"give the agent a button, not a steering wheel\" pattern.\n\n---\n\n## System prompt vs tool description\n\n| Belongs in the **system prompt** | Belongs in the **tool's description** |\n|---|---|\n| Persona, role, voice | What this specific tool does |\n| Global output/format rules (\"respond in markdown\") | When to use it vs other tools |\n| Refusal / safety behavior | What each parameter means and its shape |\n| Display protocols (`![]()` for images) | Examples of good vs bad invocations |\n| Universal context (current date via `$now`, user role) | Tool-specific gotchas (rate limits, edge cases) |\n| Inter-tool flow (\"after generating, always display\") | Tool-specific input transformations |\n\nWhy split it: a well-described tool works in **any** agent that drops it in, tool details only \"load\" when the model considers that tool (token efficiency), and you update one tool description instead of a paragraph buried in a 5000-token prompt. → **SYSTEM_PROMPT.md**\n\n---\n\n## Structured output: when and how\n\nAdd an `outputParserStructured` sub-node (wired `ai_outputParser`) when downstream needs strict JSON, not free-form text. Two rules:\n\n1. **Use `schemaType: 'manual'` with a real JSON Schema, not `jsonSchemaExample`.** An example can't express required-vs-optional, enums, numeric ranges, or array constraints — you outgrow it the first time the shape gets non-trivial. Reach for `fromJson` + an example only for throwaway shapes.\n2. **`autoFix: true` with a coding-capable fixer model.** Wire a *second* model into the parser's `ai_languageModel` slot. Reconciling broken JSON against a schema is a coding task — a weak fixer just produces another malformed retry and burns tokens.\n\n→ **STRUCTURED_OUTPUT.md** for the schema patterns, the load-bearing \"DO NOT wrap in markdown\" retry line, and the parse-failure cookbook.\n\n---\n\n## Memory: brief mental model\n\nMemory is a sub-node (`ai_memory`). Without it, every call is stateless — correct for one-shot tasks (classify, summarize). With it, the agent holds a conversation, keyed by whatever expression you bind to `sessionKey`.\n\n- **`memoryBufferWindow`** — keeps the last N exchanges per key and persists across executions via n8n's store. The default for chat. **`contextWindowLength` defaults to 5, which is very low** — 50 is a saner starting point. Messages past the window are gone entirely.\n- **`memoryPostgresChat` / `memoryRedisChat`** — only when memory must be read *outside* the agent (your own UI, analytics, cross-system). Not needed just to survive restarts; BufferWindow already does that.\n\n**Plumb a stable key from the trigger to memory consistently.** Chat triggers fill `sessionId` automatically; for other surfaces derive one (Slack `thread_ts`, a webhook conversation ID). Never hardcode `sessionId: 'default'` and never put `sessionId` behind `$fromAI` (the model will fabricate a UUID). → **MEMORY.md**\n\n---\n\n## Binary and the agent boundary\n\nThis is the seam that trips people up:\n\n- **The model CAN see uploaded images** (vision) via `options.passthroughBinaryImages: true` on the agent.\n- **Tools CANNOT receive binary.** `$fromAI()` is JSON-only — no base64, no bytes, even through non-AI bindings.\n- **The agent's output is text-shaped** (or structured-text with a parser). When a model returns image/audio/video bytes, the Agent doesn't surface them at all — there's nothing to recover downstream.\n\n**Workaround:** pre-stage uploads to storage before the agent runs, inject the storage keys into the system prompt, and let tools accept the key as a string parameter and re-fetch internally. For one-shot media generation, skip the agent and call the provider's native single-call node directly.\n\nThe binary mechanics (which storage, how to stage, how to re-fetch) are owned by **n8n-binary-and-data** — see its agent-tool binary reference. This skill only marks the boundary; don't re-derive the mechanics here.\n\n---\n\n## Human review (gate destructive tools)\n\nWhen a tool's effect needs human sign-off before execution (sends, payments, refunds, account changes), wrap it with a review tool node — `slackHitlTool`, `discordHitlTool`, `telegramHitlTool`, `gmailHitlTool`, etc. (n8n names these \"Hitl\" / human-in-the-loop). The review node sits **between** the wrapped tool and the agent on the `ai_tool` connection: wrapped tool → review node → Agent.\n\nWhether sign-off is needed is a product/policy call — **surface the question to the user**, recommend based on blast radius, and let them decide.\n\n**The critical rule: show the actual parameters the wrapped tool will receive.** Use the literal `{{ $tool.parameters.<name> }}` in the approval message, never a `$fromAI()` paraphrase — otherwise the human approves text the model made up, not the call about to fire. → **HUMAN_REVIEW.md**\n\n---\n\n## Chat agents (Slack, Discord, Teams, Telegram)\n\n**The one non-negotiable, regardless of complexity:** any chat-triggered workflow that posts a reply MUST **filter out the bot's own user ID**, or its own replies re-trigger it in an infinite loop that burns runs and tokens. Prefer trigger-level filtering when available (Slack Trigger's `options.userIds` is an **exclusion list** — put the bot ID there); otherwise filter `$json.user !== '<BOT_USER_ID>'` in the first node after the trigger.\n\nBeyond the filter, a simple bot (trigger → agent → reply) lives fine in one workflow. Split into **shell + core + sub-agents** only once you need loading UX, sub-agents, multi-surface reuse, or robust error handling:\n\n- **Shell** — trigger, anti-loop filter, event-type Switch, loading/error UX, renders the reply. No LLM.\n- **Core** — stateless agent, `chatInput` + `threadId` inputs, memory keyed on `threadId`, tools and sub-agents.\n- **Sub-agents** — one narrow domain each, called via `.toolWorkflow`, **stateless** (full context in `chatInput`).\n\n→ **CHAT_AGENT_PATTERNS.md** for per-surface semantics, threading-as-session, and the full topology.\n\n---\n\n## Persisted n8n Agents (n8n_manage_agents)\n\nA **persisted n8n Agent** is a different artifact from the AI Agent node covered above: a standalone assistant record — model, instructions, tools, skills, tasks, memory, channels — stored and versioned by n8n itself, managed through `n8n_manage_agents` (n8n's instance-level MCP server), not a node inside a workflow's JSON.\n\n| You need to… | Use |\n|---|---|\n| A reasoning step inside a workflow, wired with `ai_*` sub-nodes | **AI Agent node** (this skill, above) |\n| A standalone assistant with its own lifecycle — draft, validate, publish, versions, channels — independent of any one workflow | **Persisted Agent** (`n8n_manage_agents`) |\n\n**Prerequisites:** `N8N_MCP_ACCESS_TOKEN` configured (separate from the Public API key) and n8n **2.34+** with the agents module enabled. The token is required for **every** action, including `reference`/`search` — without it, nothing works. Separately, `reference` and `search` work for **any** agent regardless of MCP exposure; every other action needs the target agent exposed to MCP (agents created through this tool are exposed automatically — the exposure gate only matters for agents that already existed before this tool touched them).\n\n**Build sequence:**\n1. `action: \"reference\"` — read the config schema and the exact mutate operations before anything else.\n2. `action: \"discover_assets\"` — list what the agent can actually be wired to. Takes `projectId` (from `n8n_list_catalog({kind: \"projects\"})`) and `kind`: `models` (with a `provider`), `integrations`, `workflows`, `subagents` or `mcpServers`. One call per kind.\n3. `action: \"create\"` — `projectId`, `name`, `config`.\n4. `action: \"mutate\"` — one resource per call (`config.patch`, `skill.upsert`/`delete`, `task.upsert`/`delete`, `customTool.upsert`/`delete`), always carrying the **latest** hash forward. Mind the two names: n8n returns it as `configHash` and expects it back as `args.baseConfigHash`. `args` are forwarded to n8n verbatim, so a near-miss on any field name comes back as `INVALID_ARGS`, not a helpful correction — which is why step 1 reads the schema first. A stale hash comes back as `STALE_CONFIG` — re-`get` and retry with the fresh one.\n5. `action: \"validate\"` — before offering to `call` or `publish`.\n6. `action: \"publish\"` — **only on the user's explicit request**, never proactively.\n\n`action: \"call\"` runs the agent with real credentials and real tools — a live execution, not a dry run. A result can carry `approvals[]` for tool calls that need a human decision; **never approve on the user's behalf** — surface them and resume only after the user decides.\n\n**Custom tools are a third code runtime — don't reuse either of the others.** A `customTool.upsert` body is **TypeScript**, and the only imports it may use are `@n8n/agents` and `zod`. This is not the Code node (JavaScript/Python, returns `[{json: …}]`) and not the AI-agent Custom Code Tool covered by **n8n-code-tool** (`@n8n/n8n-nodes-langchain.toolCode`, returns a string, no `$fromAI()`). Reaching for the wrong contract is the easy mistake here, because all three are \"write code the agent calls\". Read the shape from `action: \"reference\"` before writing one; a compile failure or an unknown `agentId` surfaces as `AGENT_TOOL_ERROR`.\n\n**Credential caveat:** on n8n 2.36.x the agents runtime rejects `azureOpenAiApi` and `aws` credentials (reported as `missing: [\"credential\"]`); the response's `hint` names the accepted types instead.\n\n**Testing without leaving debris:** name throwaway agents `[TEST] …` and `delete` them when you're done — a persisted Agent outlives the conversation that made it, unlike a workflow you can leave inactive.\n\n→ **n8n-mcp-tools-expert** `## Agents` for the tool's full action list and error codes.\n\n---\n\n## RAG (retrieval augmented generation)\n\nn8n ships the LangChain RAG primitives (document loaders, splitters, embeddings, vector stores, retrievers). Two opinions worth stating up front:\n\n1. **Rule out cheaper lookups first.** Exact lookups → a database or Data Table query, not RAG. Freshness → a live search tool. A small/structured doc set → give the agent list/fetch tools. Reach for a vector store only when there are too many docs to list and queries are semantic.\n2. **Wire the vector store as a retrieval tool** (`mode: 'retrieve-as-tool'`, `ai_tool`) so the agent decides when retrieval is relevant and can phrase the query itself. Embed query and documents with the **same** model.\n\n→ **RAG.md** (intentionally thin — defaults depend on data shape and scale).\n\n---\n\n## Reference files\n\n| File | Read when |\n|---|---|\n| **TOOLS.md** | Adding tools, choosing among the four types, writing names/descriptions, `$fromAI` anatomy |\n| **SUBWORKFLOW_AS_TOOL.md** | Wiring a sub-workflow as a tool via `.toolWorkflow`, mapping agent-filled vs plumbed params |\n| **SYSTEM_PROMPT.md** | Writing/refactoring a system prompt, the system-prompt-vs-tool-description split |\n| **STRUCTURED_OUTPUT.md** | Forcing JSON output, configuring autoFix, the fixer model, parse-failure fixes |\n| **MEMORY.md** | Choosing a memory type, persistence, sessionId handling |\n| **HUMAN_REVIEW.md** | Adding human approval, approval-message content, multi-channel approver |\n| **CHAT_AGENT_PATTERNS.md** | Building a Slack/Discord/Teams/Telegram bot, shell + core + sub-agents topology |\n| **RAG.md** | Retrieval-augmented agents (thin by design) |\n| **EXAMPLES.md** | Concrete node-object snippets: stateless agent core, Slack router shell, domain sub-agent |\n\n---\n\n## Anti-patterns\n\n| Anti-pattern | What goes wrong | Fix |\n|---|---|---|\n| Generic tool names (`tool1`, `doStuff`, `runQuery`) | Model can't tell which tool to pick — skips them or hallucinates params | Verb-first specific names: `Search customer database`, `Generate image with Veo` |\n| Empty or one-line tool descriptions | Model has no idea when to invoke; bad selection, no error | Write a real description: what it does, when to use, what each param means |\n| Cramming per-tool instructions into the system prompt | Bloated prompt, no reuse, per-tool guidance buried | Move tool-specific instructions into tool descriptions |\n| Agent + Switch to route on natural language | Two nodes + prompt boilerplate where Text Classifier is one node | Use Text Classifier — each category gets its own output handle (name **and** description) |\n| Wrapping image/audio/video generation in an Agent | Binary doesn't flow through tools or out of the agent output | Use the provider's native single-call node directly |\n| `outputParserStructured` without `autoFix` | One malformed response halts the workflow | `autoFix: true` + a coding-capable fixer model |\n| Passing binary directly to a tool | Doesn't work — binary can't cross the tool boundary | Pre-stage to storage, pass keys; see **n8n-binary-and-data** |\n| Hardcoded `sessionId` / no sessionId / `sessionId` behind `$fromAI` | Conversations cross, or the model fabricates a UUID | Plumb a stable key from the trigger to memory and tools |\n| Two near-identical tools | Selection is non-deterministic, model gets confused | One tool with internal branching driven by a parameter |\n| Chat bot with no bot-user filter | Its own replies re-trigger it → infinite loop | Exclude the bot user ID at the trigger or first node |\n| `maxIterations` left at the low default on a multi-tool agent | \"Max iterations reached\" / empty output | Raise `options.maxIterations` |\n| Filling the human-review message via `$fromAI()` | Approver signs off on a paraphrase, not the real call | Use literal `{{ $tool.parameters.<name> }}` |\n\n---\n\n## What's NOT available via the community MCP\n\n| Want to do | Reality |\n|---|---|\n| Chat-test a workflow's AI Agent node end-to-end interactively | `n8n_test_workflow` runs the workflow, but a true multi-turn chat session against the node is a UI activity (canvas chat tester). A persisted Agent, by contrast, can be run live via `n8n_manage_agents` `call` — see \"Persisted n8n Agents\" above. |\n| Set credentials' actual secret values | `n8n_manage_credentials` creates/updates credential records, but the agent provider keys themselves are entered/verified in the UI. |\n| Assign a workflow's Error Workflow | UI only — see **n8n-error-handling**. Build the catch-all, then hand the user the UI step. |\n| Pin the exact model availability per instance | Model lists shift between versions — `search_nodes`/`get_node` reflect what's installed. Verify on the target instance. |\n\nWhat the MCP **can** do: search and inspect every LangChain node (`search_nodes`, `get_node`), validate node config and the whole graph (`validate_node`, `validate_workflow`), build and patch the agent and its sub-nodes (`n8n_update_partial_workflow` with `addConnection` on `ai_*` outputs), test (`n8n_test_workflow`), and pull the saved JSON to verify wiring (`n8n_get_workflow`). The deep AI-agent guide also lives in `tools_documentation({topic: \"ai_agents_guide\", depth: \"full\"})`.\n\n---\n\n## Integration with other skills\n\n- **n8n-workflow-patterns** (`ai_agent_workflow.md`) — the high-level \"agent in a workflow\" shape. This skill is the deep dive; start there for architecture.\n- **n8n-mcp-tools-expert** — node-type formats (short form for `get_node`, long form in JSON) and tool-selection guidance. Consult before any MCP call.\n- **n8n-node-configuration** — `displayOptions`-driven fields on the agent and sub-nodes; Slack/Block Kit message shapes (`NODE_FAMILY_GOTCHAS.md`, Slack section).\n- **n8n-expression-syntax** — `{{ }}`, `$json.output`, `$now`, and `$fromAI`/`$tool.parameters` all rely on correct expression syntax.\n- **n8n-code-tool** — the Custom Code Tool's runtime contract (string in/out, no `$fromAI`). Read it before writing a `.toolCode`.\n- **n8n-subworkflows** — the sub-workflow primitive that `.toolWorkflow` builds on (Execute Workflow Trigger inputs/outputs, naming, search-before-build).\n- **n8n-binary-and-data** — owns the agent-tool binary boundary mechanics (staging uploads, returning generated files).\n- **n8n-validation-expert** — interpreting `validate_workflow` results, including AI-connection issues (a tool wired into `main` instead of `ai_tool` flags as disconnected).\n- **n8n-error-handling** — `onError: 'continueErrorOutput'` on tool sub-workflows and the agent-core call; error UX on chat shells.\n- **n8n-code-javascript / n8n-code-python** — for Code-node logic *inside* a tool sub-workflow (different sandbox from the Code Tool).\n\n---\n\n## Quick reference checklist\n\nBefore shipping an agent:\n\n- [ ] **Right node**: Agent for tools/memory/multi-turn; Text Classifier for routing; Information Extractor for fields; native node for media\n- [ ] **Model** wired via `ai_languageModel`\n- [ ] **Every tool** has a verb-first specific name AND a real description\n- [ ] **`$fromAI()` descriptions** are specific (format, range, example); identity/limits/sessionId plumbed deterministically, not via `$fromAI`\n- [ ] **Per-tool guidance** lives in tool descriptions, not the system prompt\n- [ ] **`$now`** in the system prompt (no hardcoded date)\n- [ ] **`maxIterations`** raised for multi-tool agents\n- [ ] **Memory** keyed on a stable `sessionKey` from the trigger (not `'default'`, not `$fromAI`); `contextWindowLength` raised from 5\n- [ ] **Structured output**: `schemaType: 'manual'` + `autoFix: true` + a coding-capable fixer model\n- [ ] **Destructive tools** wrapped in human review; approval message uses `$tool.parameters`, not `$fromAI`\n- [ ] **Chat bots** filter the bot's own user ID (trigger-level or first node)\n- [ ] **Binary**: model vision via `passthroughBinaryImages`; tools get storage keys, never bytes\n- [ ] **Validated** with `validate_workflow` and verified with `n8n_get_workflow` (sub-nodes on `ai_*`, not `main`)\n\n---\n\n**Remember**: an agent is only as good as its tool names, descriptions, and system-prompt discipline. The model can't see your wiring — it sees a system prompt and a list of named, described tools. Design those like an API and most \"the agent won't behave\" problems disappear.\n","frontmatter":{"name":"n8n-agents","description":"Design n8n AI agents the right way. Use when building or editing any @n8n/n8n-nodes-langchain.* AI node — an AI Agent, LLM chain, Text Classifier, or Information Extractor — and whenever the user mentions AI agents, LLM with tools, tool calling, $fromAI, system prompts, agent memory, sessionId, structured/JSON output, output parser, RAG, vector store, a chat assistant/bot, or human-in-the-loop review. Covers Agent-vs-chain-vs-classifier choice, the model/memory/tools/outputParser slots, tool names/descriptions as prompt, structured output with autoFix, memory, RAG, human review, and chat topologies."},"isInternal":false,"tokens":6623,"sizeBytes":27308},{"name":"STRUCTURED_OUTPUT.md","path":"data/skills/n8n-agents/STRUCTURED_OUTPUT.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-agents/STRUCTURED_OUTPUT.md","title":"N8n-agents Skill","category":"anthropic-skill","format":"markdown","content":"# N8n-agents Skill\n\nPath: `data/skills/n8n-agents/STRUCTURED_OUTPUT.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-agents/STRUCTURED_OUTPUT.md)","tokens":300,"sizeBytes":205},{"name":"SUBWORKFLOW_AS_TOOL.md","path":"data/skills/n8n-agents/SUBWORKFLOW_AS_TOOL.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-agents/SUBWORKFLOW_AS_TOOL.md","title":"N8n-agents Skill","category":"anthropic-skill","format":"markdown","content":"# N8n-agents Skill\n\nPath: `data/skills/n8n-agents/SUBWORKFLOW_AS_TOOL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-agents/SUBWORKFLOW_AS_TOOL.md)","tokens":300,"sizeBytes":209},{"name":"SYSTEM_PROMPT.md","path":"data/skills/n8n-agents/SYSTEM_PROMPT.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-agents/SYSTEM_PROMPT.md","title":"N8n-agents Skill","category":"anthropic-skill","format":"markdown","content":"# N8n-agents Skill\n\nPath: `data/skills/n8n-agents/SYSTEM_PROMPT.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-agents/SYSTEM_PROMPT.md)","tokens":300,"sizeBytes":197},{"name":"TOOLS.md","path":"data/skills/n8n-agents/TOOLS.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-agents/TOOLS.md","title":"N8n-agents Skill","category":"anthropic-skill","format":"markdown","content":"# N8n-agents Skill\n\nPath: `data/skills/n8n-agents/TOOLS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-agents/TOOLS.md)","tokens":300,"sizeBytes":181},{"name":"AGENT_TOOL_BINARY.md","path":"data/skills/n8n-binary-and-data/AGENT_TOOL_BINARY.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-binary-and-data/AGENT_TOOL_BINARY.md","title":"N8n-binary-and-data Skill","category":"anthropic-skill","format":"markdown","content":"# N8n-binary-and-data Skill\n\nPath: `data/skills/n8n-binary-and-data/AGENT_TOOL_BINARY.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-binary-and-data/AGENT_TOOL_BINARY.md)","tokens":300,"sizeBytes":232},{"name":"BINARY_BASICS.md","path":"data/skills/n8n-binary-and-data/BINARY_BASICS.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-binary-and-data/BINARY_BASICS.md","title":"N8n-binary-and-data Skill","category":"anthropic-skill","format":"markdown","content":"# N8n-binary-and-data Skill\n\nPath: `data/skills/n8n-binary-and-data/BINARY_BASICS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-binary-and-data/BINARY_BASICS.md)","tokens":300,"sizeBytes":224},{"name":"CDN_REQUIREMENT.md","path":"data/skills/n8n-binary-and-data/CDN_REQUIREMENT.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-binary-and-data/CDN_REQUIREMENT.md","title":"N8n-binary-and-data Skill","category":"anthropic-skill","format":"markdown","content":"# N8n-binary-and-data Skill\n\nPath: `data/skills/n8n-binary-and-data/CDN_REQUIREMENT.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-binary-and-data/CDN_REQUIREMENT.md)","tokens":300,"sizeBytes":228},{"name":"MERGE_FOR_CONTEXT.md","path":"data/skills/n8n-binary-and-data/MERGE_FOR_CONTEXT.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-binary-and-data/MERGE_FOR_CONTEXT.md","title":"N8n-binary-and-data Skill","category":"anthropic-skill","format":"markdown","content":"# N8n-binary-and-data Skill\n\nPath: `data/skills/n8n-binary-and-data/MERGE_FOR_CONTEXT.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-binary-and-data/MERGE_FOR_CONTEXT.md)","tokens":300,"sizeBytes":232},{"name":"README.md","path":"data/skills/n8n-binary-and-data/README.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-binary-and-data/README.md","title":"N8n-binary-and-data Skill","category":"anthropic-skill","format":"markdown","content":"# N8n-binary-and-data Skill\n\nPath: `data/skills/n8n-binary-and-data/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-binary-and-data/README.md)","tokens":300,"sizeBytes":210},{"name":"SKILL.md","path":"data/skills/n8n-binary-and-data/SKILL.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-binary-and-data/SKILL.md","title":"n8n-binary-and-data","category":"anthropic-skill","format":"markdown","content":"---\nname: n8n-binary-and-data\ndescription: Handle files and binary data in n8n correctly. Use when working with files, images, PDFs, attachments, uploads or downloads, base64, vision/multimodal input, or when an AI agent needs a file as tool input or output — and whenever the user mentions $binary, binaryPropertyName, \"read the PDF\", \"attach the file\", \"send the image\", Merge losing binary, or a CDN for chat images. Covers the $binary vs $json split, reading/writing binary, keeping binary alive across transforms with Merge, the agent-tool binary boundary, and the CDN/URL requirement for chat surfaces.\n---\n\n# n8n Binary and Data\n\nEvery n8n item carries two independent slots: `$json` for structured data and `$binary` for file bytes. They travel side by side through the workflow. File contents — the actual PDF, image, or zip — live in `$binary`, never in `$json`. Get that split wrong and you read an empty field, lose a file mid-flow, or hand an AI agent a tool input it can't use.\n\nThis skill covers where binary lives, how to read and write it, how to keep it from being silently stripped, the hard wall between binary and the AI-agent tool boundary, and why chat surfaces need a URL instead of raw bytes.\n\n---\n\n## The three rules that prevent 90% of binary bugs\n\n1. **File contents are in `$binary`, not `$json`.** After an HTTP download, a \"Read Files\", or an email-attachment trigger, the bytes sit in `$binary.<key>`. `$json` holds metadata at most. Reading `$json.data` for file contents gives you nothing.\n\n2. **Binary cannot cross the AI-agent tool boundary — in either direction.** Tool arguments and tool return values are JSON only. An uploaded image can't be passed into a tool as a file, and a tool can't return raw bytes. Pre-stage to storage and pass a key or URL through JSON instead. See `AGENT_TOOL_BINARY.md`.\n\n3. **Chat surfaces render images by URL, not by `$binary`.** Slack, Discord, Teams, Telegram, embedded webhook chat — none of them read the binary slot. The image has to live somewhere a URL can fetch it. See `CDN_REQUIREMENT.md`.\n\n---\n\n## The two slots\n\nEach item is shaped like this:\n\n```json\n{\n  \"json\": { \"customerId\": 42, \"status\": \"sent\" },\n  \"binary\": {\n    \"invoice\": {\n      \"data\": \"<base64-encoded bytes>\",\n      \"mimeType\": \"application/pdf\",\n      \"fileName\": \"invoice-42.pdf\",\n      \"fileExtension\": \"pdf\"\n    }\n  }\n}\n```\n\nThe key inside `binary` (`invoice` here) is the **binary property name**. Most file-handling nodes have a `binaryPropertyName` parameter that points at it — the producer names the slot, the consumer references it by that name. The default key across most nodes is `data`, so when nothing tells you otherwise, assume `$binary.data`.\n\n`$json` and `$binary` are separate namespaces. An expression like `{{ $binary.invoice.fileName }}` reads file metadata; `{{ $json.customerId }}` reads data. They never mix.\n\nThis split also explains a webhook gotcha: a Webhook trigger receiving `multipart/form-data` puts the uploaded file in `$binary` and the accompanying form fields in `$json.body` — so an uploaded file is not somewhere under `$json` at all. (The `$json.body` nesting for webhooks is **n8n-expression-syntax** territory.)\n\nSee `BINARY_BASICS.md` for the full slot anatomy, mime types, and size limits.\n\n---\n\n## Producing binary\n\nYou rarely build a `$binary` slot by hand — nodes populate it for you:\n\n| Source | How binary appears |\n|---|---|\n| HTTP Request with `responseFormat: \"file\"` | Response body lands in `$binary.data` (or the name you set) |\n| Read/Write Files from Disk | File contents read into `$binary` |\n| Storage downloads (S3, Google Drive, Dropbox, etc.) | Downloaded file in `$binary.<key>` |\n| Email triggers with attachments | Each attachment arrives in `$binary` |\n| Provider AI media nodes (image/audio gen) | Set `options.binaryPropertyOutput` so the bytes land where the next node looks |\n\nFor an HTTP download, the one field that matters is `responseFormat`. Confirm it with `get_node` on `nodes-base.httpRequest` — leaving it as the default JSON/string format is the classic reason a downloaded file ends up as garbled text in `$json` instead of clean bytes in `$binary`.\n\n---\n\n## Reading and writing binary in a Code node\n\nMost workflows never need to crack open the bytes — they just pass binary through to a consumer (email attachment, file upload, Slack file). When you do need the raw bytes, do it in a Code node.\n\n**Read** with `getBinaryDataBuffer` — do not try to base64-decode `$binary.<key>.data` by hand:\n\n```javascript\n// Code node, \"Run Once for Each Item\"\nconst buffer = await this.helpers.getBinaryDataBuffer(0, 'data'); // (itemIndex, propertyName)\nconst text = buffer.toString('utf-8');\nconst length = buffer.length;\n\nreturn [{\n  json: { ...$json, length },\n  binary: $input.item.binary,   // pass the binary through, or it's gone\n}];\n```\n\n**Write** by building the slot yourself — base64 the bytes plus a mime type and file name:\n\n```javascript\nconst text = 'Hello, world!';\nreturn [{\n  json: { ok: true },\n  binary: {\n    report: {\n      data: Buffer.from(text).toString('base64'),\n      mimeType: 'text/plain',\n      fileName: 'report.txt',\n      fileExtension: 'txt',\n    },\n  },\n}];\n```\n\nThe Code-node sandbox, helpers, and execution modes are the domain of **n8n-code-javascript** (and **n8n-code-python**) — use those for the language-level detail. The one binary-specific thing to remember here: a Code node that returns `[{ json: {...} }]` without re-attaching `binary` **silently drops the file**. See `BINARY_BASICS.md`.\n\n---\n\n## Keeping binary alive across transforms\n\nJSON-only nodes — Edit Fields (Set), Code, IF, and others — can drop the `$binary` slot from their output. The workflow validates clean and runs without error; the file just isn't there downstream when the email node goes to attach it.\n\nTwo ways to keep it:\n\n- **Pass-through option on the transforming node.** Edit Fields has `includeOtherFields`; a Code node can return `binary: $input.item.binary` explicitly. Cheapest fix when it's available.\n- **Fan out and Merge by position.** Route the source into both the transform and a bypass branch, then recombine with a Merge in `combineByPosition` mode. The JSON comes from the transform side, the binary survives on the bypass side.\n\n```\n[Source with binary] ─┬─→ [Edit Fields: change JSON] ─┐\n                      │      (binary stripped here)     ├─→ [Merge: combineByPosition] ─→ [Email: attach]\n                      └──────────────────────────────────┘\n                          (bypass — binary passes through untouched)\n```\n\n`combineByPosition` pairs item N from each input, so the field counts must line up. The connection wiring and the alternatives for many-strip-point chains (upload-early, sub-workflow) are in `MERGE_FOR_CONTEXT.md`.\n\n---\n\n## The agent-tool binary boundary\n\nThis is the sharpest edge. An AI Agent talks to its tools (Custom Code Tool, Call n8n Workflow Tool, HTTP Request Tool, MCP tools) over JSON. Binary does not fit through that pipe in either direction. The fix is the same shape both ways: **stage the bytes in storage, pass a key/URL through JSON, fetch on the other side.**\n\n**Inbound — a user uploads a file the agent's tool must operate on:**\n\n1. The chat trigger gives you a `files[]` array. Split it out and upload each file to private storage under a hashed key.\n2. Re-merge that branch before the agent runs (it's a synchronization barrier, not decoration), and set `executeOnce: true` on the agent so N files don't trigger N agent runs.\n3. Inject the keys into the agent's system prompt, listing both the original name (human context) and the storage key (what the tool needs), with an explicit \"use EXACTLY this key\".\n4. The tool receives the key as a string argument and downloads the file from storage itself.\n\n**Outbound — a tool generates a file the agent must return:**\n\n1. The tool sub-workflow generates the binary, uploads it to storage, and returns JSON like `{ \"ok\": true, \"key\": \"...\", \"url\": \"https://...\", \"mimeType\": \"image/png\" }`.\n2. The agent embeds the URL in its reply (or passes the key to another tool).\n\n`passthroughBinaryImages: true` on the agent only changes what the **LLM sees** for vision — it does **not** let tools receive the file, and it's image-only (no PDFs, audio, or video). You still need the upload-and-pass-key pattern for any tool. Full patterns, hash strategy, storage choices, and the long-running-tool variant are in `AGENT_TOOL_BINARY.md`.\n\n> Building the tool itself? See **n8n-code-tool** for the Custom Code Tool contract and **n8n-workflow-patterns** for the AI-Agent-with-tools shape.\n\n---\n\n## The CDN requirement for chat surfaces\n\nWhen a workflow generates an image and the user wants it shown inside a chat message:\n\n- **Binary on the item isn't enough.** The chat client renders messages that reference images by URL (or pushes bytes through the platform's own file-upload API). It never reads `$binary`.\n- **The bytes have to live somewhere a URL can fetch over HTTPS.** Upload to an object store or drive first, then embed the returned URL.\n- **n8n has no built-in CDN.** The user provides the storage.\n\nAsk which storage they already use rather than defaulting to S3 — object storage (S3, R2, GCS, Azure Blob, Backblaze B2, Supabase Storage) and drive-style services (Dropbox, Google Drive, OneDrive, Box) all work and all change the URL shape. Cloudflare R2 is the lowest-friction starting point if they have nothing. For sensitive content, use a signed URL with an expiry rather than a permanently public one. See `CDN_REQUIREMENT.md`.\n\n---\n\n## What's NOT available\n\n- **`$fromAI()` cannot carry binary.** It fills tool parameters with strings, numbers, booleans, and objects — never file bytes. Pass a storage key instead.\n- **Tool arguments and returns are JSON only.** There is no \"binary parameter\" on an agent tool, in or out.\n- **n8n ships no CDN or public file host.** Serving a file over a URL is always something the user's storage does, not n8n.\n- **`getBinaryDataBuffer` is a Code-node helper.** It isn't available in the Custom Code Tool sandbox (see **n8n-code-tool**).\n\n---\n\n## Where Data Tables live\n\nFor persistent tabular storage — reference-counting staged files, tracking which keys are live, dedup — that's the `n8n_manage_datatable` surface, owned by **n8n-mcp-tools-expert**. This skill does not cover Data Tables.\n\n---\n\n## Anti-patterns\n\n| Anti-pattern | What goes wrong | Fix |\n|---|---|---|\n| Reading file contents from `$json` | Bytes live in `$binary`; `$json` is empty or metadata only | Read `$binary.<key>`, or `getBinaryDataBuffer` in a Code node |\n| HTTP download without `responseFormat: \"file\"` | Bytes arrive as mangled text in `$json`, not clean binary | Set `responseFormat: \"file\"` on the HTTP Request node |\n| Code node returns `[{json:{...}}]`, no `binary` | The file is silently dropped downstream | Re-attach `binary: $input.item.binary` in the return |\n| JSON transform (Edit Fields/IF) eats the binary | Email/upload node finds nothing to attach | Pass-through option, or fan out + Merge by position |\n| Passing an uploaded file into a tool via `$fromAI` | `$fromAI` can't carry binary; the tool gets nothing | Pre-stage to storage, inject the key in the system prompt, tool fetches by key |\n| Assuming `passthroughBinaryImages` lets tools see the file | It only affects what the LLM sees, and only for images | Still need the upload-and-pass-key pattern for tools |\n| Tool returns raw binary to the agent | Tool output is JSON; bytes don't survive (and bloat context) | Upload, return `{ key, url }` in JSON |\n| Posting `$binary` to a chat surface and expecting an image | Chat clients render by URL, not raw bytes | Upload to storage/CDN, embed the URL or use the platform file API |\n| Hardcoding base64 in a Code node | Huge workflow JSON, slow, leaky | Reference via `$binary`, or upload and reference by URL |\n\n---\n\n## Reference files\n\n| File | Read when |\n|---|---|\n| `BINARY_BASICS.md` | First time handling binary, or reading/writing the `$binary` slot, mime types, size limits |\n| `AGENT_TOOL_BINARY.md` | An agent tool needs an uploaded file, or produces one — the boundary in either direction |\n| `MERGE_FOR_CONTEXT.md` | Binary disappears after a JSON transform and you need to re-attach it |\n| `CDN_REQUIREMENT.md` | Showing images in a chat surface or anywhere that needs URL-referenced images |\n\n---\n\n## Integration with Other Skills\n\n**n8n-code-javascript / n8n-code-python**: the Code node is where you read/write raw bytes (`getBinaryDataBuffer`, `Buffer.from(...).toString('base64')`). Those skills own the sandbox, helpers, and execution-mode detail — this skill owns the rule that binary must be re-attached on return.\n\n**n8n-code-tool**: the Custom Code Tool sandbox is narrower — no `$binary`, no `getBinaryDataBuffer`, no `$fromAI`. When a tool needs a file, this skill's storage-key pattern is how it gets one.\n\n**n8n-workflow-patterns**: the agent-tool binary boundary sits inside the AI-Agent-with-tools pattern; the CDN flow is a generate → upload → reply chain.\n\n**n8n-node-configuration**: `responseFormat`, `binaryPropertyName`, `includeOtherFields`, `binaryPropertyOutput` are all conditional fields — use `get_node` to confirm the exact names on the user's version.\n\n**n8n-expression-syntax**: addressing `$binary.<key>.fileName` vs `$json.body` (webhook uploads in particular) is expression territory.\n\n**n8n-validation-expert**: a dropped binary slot is a silent failure — `validate_workflow` won't flag it. Confirm presence by inspecting the execution.\n\n**n8n-mcp-tools-expert**: owns `n8n_manage_datatable` (Data Tables) and `n8n_executions` — use the latter to confirm a `binary` slot actually survived a given node.\n\n**n8n-error-handling**: storage uploads and downloads fail; the inbound/outbound staging steps need error branches so a missing key doesn't 404 silently.\n\n**using-n8n-mcp-skills**: the index of how these skills fit together.\n\n---\n\n## Verifying binary survived\n\nValidation won't catch a stripped binary slot — it's a silent failure. Confirm it ran correctly:\n\n1. `n8n_test_workflow` (or trigger a real run) to produce an execution.\n2. `n8n_executions` to pull that execution, and inspect per-node output for the `binary` slot — it shows presence and metadata even if the base64 is too large to render.\n3. The node where `binary` last appears is the node before the strip. That's where the pass-through or Merge goes.\n\n---\n\n## Quick Reference Checklist\n\n- [ ] File contents read from `$binary.<key>` — never `$json`\n- [ ] HTTP downloads use `responseFormat: \"file\"`\n- [ ] Code nodes re-attach `binary` on return when the file must continue\n- [ ] JSON transforms either pass binary through or Merge it back (`combineByPosition`)\n- [ ] No attempt to pass binary into/out of an agent tool — keys/URLs through JSON instead\n- [ ] `passthroughBinaryImages` used only for LLM vision, not as a tool channel\n- [ ] Chat-surface images uploaded to storage; the URL is embedded, not the bytes\n- [ ] Storage backend chosen with the user (not defaulted to S3); signed URLs for sensitive content\n- [ ] Binary presence confirmed by inspecting the execution, not by validation\n\n---\n\n**Remember**: two slots, side by side. Data rides in `$json`, files ride in `$binary` — and the moment a file has to cross an agent tool or reach a chat surface, it travels as a URL, not as bytes.\n","frontmatter":{"name":"n8n-binary-and-data","description":"Handle files and binary data in n8n correctly. Use when working with files, images, PDFs, attachments, uploads or downloads, base64, vision/multimodal input, or when an AI agent needs a file as tool input or output — and whenever the user mentions $binary, binaryPropertyName, \"read the PDF\", \"attach the file\", \"send the image\", Merge losing binary, or a CDN for chat images. Covers the $binary vs $json split, reading/writing binary, keeping binary alive across transforms with Merge, the agent-tool binary boundary, and the CDN/URL requirement for chat surfaces."},"isInternal":false,"tokens":3699,"sizeBytes":15591},{"name":"BUILTIN_FUNCTIONS.md","path":"data/skills/n8n-code-javascript/BUILTIN_FUNCTIONS.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-code-javascript/BUILTIN_FUNCTIONS.md","title":"N8n-code-javascript Skill","category":"anthropic-skill","format":"markdown","content":"# N8n-code-javascript Skill\n\nPath: `data/skills/n8n-code-javascript/BUILTIN_FUNCTIONS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-code-javascript/BUILTIN_FUNCTIONS.md)","tokens":300,"sizeBytes":232},{"name":"COMMON_PATTERNS.md","path":"data/skills/n8n-code-javascript/COMMON_PATTERNS.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-code-javascript/COMMON_PATTERNS.md","title":"N8n-code-javascript Skill","category":"anthropic-skill","format":"markdown","content":"# N8n-code-javascript Skill\n\nPath: `data/skills/n8n-code-javascript/COMMON_PATTERNS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-code-javascript/COMMON_PATTERNS.md)","tokens":300,"sizeBytes":228},{"name":"DATA_ACCESS.md","path":"data/skills/n8n-code-javascript/DATA_ACCESS.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-code-javascript/DATA_ACCESS.md","title":"N8n-code-javascript Skill","category":"anthropic-skill","format":"markdown","content":"# N8n-code-javascript Skill\n\nPath: `data/skills/n8n-code-javascript/DATA_ACCESS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-code-javascript/DATA_ACCESS.md)","tokens":300,"sizeBytes":220},{"name":"ERROR_PATTERNS.md","path":"data/skills/n8n-code-javascript/ERROR_PATTERNS.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-code-javascript/ERROR_PATTERNS.md","title":"N8n-code-javascript Skill","category":"anthropic-skill","format":"markdown","content":"# N8n-code-javascript Skill\n\nPath: `data/skills/n8n-code-javascript/ERROR_PATTERNS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-code-javascript/ERROR_PATTERNS.md)","tokens":300,"sizeBytes":226},{"name":"README.md","path":"data/skills/n8n-code-javascript/README.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-code-javascript/README.md","title":"N8n-code-javascript Skill","category":"anthropic-skill","format":"markdown","content":"# N8n-code-javascript Skill\n\nPath: `data/skills/n8n-code-javascript/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-code-javascript/README.md)","tokens":300,"sizeBytes":210},{"name":"SKILL.md","path":"data/skills/n8n-code-javascript/SKILL.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-code-javascript/SKILL.md","title":"n8n-code-javascript","category":"anthropic-skill","format":"markdown","content":"---\nname: n8n-code-javascript\ndescription: Write JavaScript code in n8n Code nodes. Use when writing JavaScript in n8n, using $input/$json/$node syntax, making HTTP requests with this.helpers / the $helpers global, working with dates using DateTime, troubleshooting Code node errors, choosing between Code node modes, or doing any custom data transformation in n8n. Always use this skill when a workflow needs a Code node — whether for data aggregation, filtering, API calls, format conversion, batch processing logic, or any custom JavaScript. Covers SplitInBatches loop patterns, cross-iteration data, pairedItem, and real-world production patterns. Also use when asked why a Code node or workflow is slow, which execution mode is faster, or how to cut per-item overhead on large datasets. EXCEPTION — for the AI-agent-callable Custom Code Tool (@n8n/n8n-nodes-langchain.toolCode, a tool attached to an AI Agent), use the n8n-code-tool skill instead; it has a different runtime contract.\n---\n\n# JavaScript Code Node\n\nExpert guidance for writing JavaScript code in n8n Code nodes.\n\n---\n\n## Quick Start\n\n```javascript\n// Basic template for Code nodes\nconst items = $input.all();\n\n// Process data\nconst processed = items.map(item => ({\n  json: {\n    ...item.json,\n    processed: true,\n    timestamp: new Date().toISOString()\n  }\n}));\n\nreturn processed;\n```\n\n### Essential Rules\n\n1. **Choose \"Run Once for All Items\" mode** (recommended for most use cases)\n2. **Access data**: `$input.all()`, `$input.first()`, or `$input.item`\n3. **Return `[{json: {...}}]`** — the canonical, mode-portable form. In *Run Once for All Items* mode n8n also auto-wraps a bare `return {…}` object, so that runs too; what genuinely fails is returning a primitive (string/number) or `null`.\n4. **CRITICAL**: Webhook data is under `$json.body` (not `$json` directly)\n5. **Built-ins available**: `this.helpers.httpRequest()` (no auth — the bare `$helpers` global is **undefined** in the task-runner sandbox, so `$helpers.httpRequest()` throws `ReferenceError: $helpers is not defined`), DateTime (Luxon), $jmespath(). **Not available**: `this.helpers.httpRequestWithAuthentication` (deny-listed), $env (when N8N_BLOCK_ENV_ACCESS_IN_NODE=true), require() (unless allowlisted). For anything beyond a trivial unauthenticated GET (auth, pagination, retries), prefer the **HTTP Request node** and keep Code nodes for pure logic.\n6. **Instance-allowlisted libraries**: Self-hosted instances can allowlist modules via `N8N_RUNNERS_ALLOWED_BUILT_IN_MODULES` and `N8N_RUNNERS_ALLOWED_EXTERNAL_MODULES` (legacy: `NODE_FUNCTION_ALLOW_BUILTIN` / `NODE_FUNCTION_ALLOW_EXTERNAL`). If the user says their instance allows specific modules (e.g. `axios`, `lodash`, `crypto`), use them via `require()` — don't refuse. If unsure, ask or default to built-ins only.\n7. **Wrong skill?** If you're writing code for a **Custom Code Tool** attached to an AI Agent (`@n8n/n8n-nodes-langchain.toolCode`), stop — that node has a different contract (input via `query`, must return a string, no `$input`/`$helpers`). Use the **n8n-code-tool** skill.\n\n---\n\n## Mode Selection Guide\n\nThe Code node offers two execution modes. Choose based on your use case:\n\n### Run Once for All Items (Recommended - Default)\n\n**Use this mode for:** 95% of use cases\n\n- **How it works**: Code executes **once** regardless of input count\n- **Data access**: `$input.all()` or `items` array\n- **Best for**: Aggregation, filtering, batch processing, transformations, API calls with all data\n- **Performance**: Faster for multiple items (single execution)\n\n```javascript\n// Example: Calculate total from all items\nconst allItems = $input.all();\nconst total = allItems.reduce((sum, item) => sum + (item.json.amount || 0), 0);\n\nreturn [{\n  json: {\n    total,\n    count: allItems.length,\n    average: total / allItems.length\n  }\n}];\n```\n\n**When to use:**\n- ✅ Comparing items across the dataset\n- ✅ Calculating totals, averages, or statistics\n- ✅ Sorting or ranking items\n- ✅ Deduplication\n- ✅ Building aggregated reports\n- ✅ Combining data from multiple items\n\n### Run Once for Each Item\n\n**Use this mode for:** Specialized cases only\n\n- **How it works**: Code executes **separately** for each input item\n- **Data access**: `$input.item` or `$item`\n- **Best for**: Item-specific logic, independent operations, per-item validation\n- **Performance**: Slower for large datasets (multiple executions)\n\n```javascript\n// Example: Add processing timestamp to each item\nconst item = $input.item;\n\nreturn [{\n  json: {\n    ...item.json,\n    processed: true,\n    processedAt: new Date().toISOString()\n  }\n}];\n```\n\n**When to use:**\n- ✅ Each item needs independent API call\n- ✅ Per-item validation with different error handling\n- ✅ Item-specific transformations based on item properties\n- ✅ When items must be processed separately for business logic\n\n**Decision Shortcut:**\n- **Need to look at multiple items?** → Use \"All Items\" mode\n- **Each item completely independent?** → Use \"Each Item\" mode\n- **Not sure?** → Use \"All Items\" mode (you can always loop inside)\n\n### Why \"All Items\" is faster — the per-item boundary\n\nMode choice is the single biggest performance lever in a Code node. Each *per-item* execution context costs a setup tax (measured on n8n 2.x, small records):\n\n| What runs per item | Approx. cost |\n|---|---|\n| Code **All Items** (one run for the whole set) | ~0.02 ms/item |\n| Expression in any node (IF / Set / etc.) | ~0.2 ms/item |\n| Code **Each Item** (a full sandbox per item) | ~0.6 ms/item — ~25–30× All Items |\n\nSo `Run Once for Each Item` over 10k items is ~6 s of pure overhead vs ~0.2 s in `Run Once for All Items`. Use Each Item only when an item genuinely needs isolating (independent error handling, or a per-item API call you can't batch); otherwise loop *inside* one All Items node. Expression complexity itself is essentially free (~90% of the cost is the per-item context, not your code) and every node→node hop re-copies all items — so reduce the *number* of per-item boundaries, don't micro-optimize each one. Below a few hundred items none of this matters; reach for it on the hot path (large item counts, little I/O).\n\n**See**: [DATA_ACCESS.md](DATA_ACCESS.md) → \"Mode Performance\" for the corollaries, hop costs, and scale check.\n\n---\n\n## Data Access Patterns\n\nFour ways to pull data from upstream nodes. Note `$node[\"Name\"]` and `$('Name')` need `.first().json` or `.all()` — never `.json` directly.\n\n```javascript\nconst allItems = $input.all();          // 1. All items — batch ops, aggregation (most common)\nconst data = $input.first().json;       // 2. First item — single objects, API responses\nconst item = $input.item;               // 3. Current item — \"Each Item\" mode ONLY (undefined otherwise)\nconst other = $node[\"Webhook\"].json;    // 4. Named node — combine data across nodes\n```\n\nAlways access fields via `.json` (e.g. `item.json.name`, not `item.name`), and prefer the explicit `$input.first().json.field` over a bare `$json.field`.\n\n**See**: [DATA_ACCESS.md](DATA_ACCESS.md) for the full guide — every pattern with examples, a decision tree, and the common mistakes (mutating originals, missing length checks, `$input.item` in the wrong mode).\n\n---\n\n## Critical: Webhook Data Structure\n\n**MOST COMMON MISTAKE**: Webhook data is nested under `.body`\n\n```javascript\n// ❌ WRONG - Will return undefined\nconst name = $json.name;\nconst email = $json.email;\n\n// ✅ CORRECT - Webhook data is under .body\nconst name = $json.body.name;\nconst email = $json.body.email;\n\n// Or with $input\nconst webhookData = $input.first().json.body;\nconst name = webhookData.name;\n```\n\n**Why**: Webhook node wraps all request data under `body` property. This includes POST data, query parameters, and JSON payloads.\n\n**See**: [DATA_ACCESS.md](DATA_ACCESS.md) for full webhook structure details\n\n---\n\n## Return Format Requirements\n\n**Canonical form**: `[{json: {...}}]` — an array of objects each with a `json` property. It is unambiguous and works identically in both execution modes, so make it your default.\n\nIn *Run Once for All Items* mode n8n auto-normalizes looser shapes on the way out: a single bare object, or an array of bare objects, gets wrapped under `json` for you. So `return {foo: 1}` runs. What has nothing to wrap — and therefore genuinely fails at runtime with \"Code doesn't return items properly\" — is a primitive (string/number/boolean) or `null`/`undefined`. (n8n-mcp ≥ 2.63.0 no longer flags a bare-object return as an error; it reflects this auto-wrap behavior.)\n\n### Correct Return Formats\n\n```javascript\n// ✅ Single result\nreturn [{\n  json: {\n    field1: value1,\n    field2: value2\n  }\n}];\n\n// ✅ Multiple results\nreturn [\n  {json: {id: 1, data: 'first'}},\n  {json: {id: 2, data: 'second'}}\n];\n\n// ✅ Transformed array\nconst transformed = $input.all()\n  .filter(item => item.json.valid)\n  .map(item => ({\n    json: {\n      id: item.json.id,\n      processed: true\n    }\n  }));\nreturn transformed;\n\n// ✅ Empty result (when no data to return)\nreturn [];\n\n// ✅ Conditional return\nif (shouldProcess) {\n  return [{json: processedData}];\n} else {\n  return [];\n}\n```\n\n### Non-Canonical Returns (auto-wrapped — prefer the canonical form)\n\n```javascript\n// ⚠️ Auto-wrapped in All Items mode → [{json: {field: value}}]. Runs, but prefer the array form.\nreturn {\n  json: {field: value}\n};\n\n// ⚠️ Auto-wrapped → [{json: {field: value}}]. Runs, but add the json wrapper for clarity.\nreturn [{field: value}];\n\n// ✅ Fine — input items already carry a json property, so returning them unchanged is a valid passthrough\nreturn $input.all();\n```\n\n### Genuinely Broken Returns\n\n```javascript\n// ❌ FAILS: primitive — n8n errors \"Code doesn't return items properly\"\nreturn \"processed\";\n\n// ❌ FAILS: null / undefined — nothing to pass to the next node\nreturn null;\n```\n\n**Why it matters**: The canonical `[{json: {...}}]` is unambiguous and behaves the same in both modes. n8n auto-normalizes bare objects and arrays-of-objects in All Items mode, but a primitive or `null` return has nothing to wrap and stops execution.\n\n**See**: [ERROR_PATTERNS.md](ERROR_PATTERNS.md) #3 for detailed error solutions\n\n---\n\n## Common Patterns Overview\n\nThe most useful Code node shapes from production workflows. One quick example — sum/aggregate across all items:\n\n```javascript\nconst items = $input.all();\nconst total = items.reduce((sum, item) => sum + (item.json.amount || 0), 0);\nreturn [{ json: { total, count: items.length, average: total / items.length } }];\n```\n\nThe full library covers 10 patterns: multi-source aggregation, regex filtering, markdown/structured-text parsing, JSON comparison, CRM/form transformation, release processing, array transformation with computed fields, Slack Block Kit formatting, top-N ranking, and string-aggregation reporting — each with variations.\n\n**See**: [COMMON_PATTERNS.md](COMMON_PATTERNS.md) for the 10 detailed production patterns (and the Best Practices section: validate input, try-catch, filter-early, array methods over loops, console.log debugging).\n\n---\n\n## Error Prevention - Top Mistakes\n\nThe recurring Code node failures, in rough frequency order:\n\n1. **Empty code / missing return** — always end with `return [...]`, and make sure *every* branch returns.\n2. **Expression syntax as code** — don't write `{{ }}` where JavaScript belongs (`return {{ $json.x }}` is a syntax error). Use `` `${$json.field}` `` or `$input.first().json.field`. `{{ }}` *inside a string literal* is fine — it's just literal text n8n won't evaluate.\n3. **Return shape** — prefer `return [{json:{...}}]`. A bare `return {…}` auto-wraps in All Items mode, but returning a primitive (string/number) or `null` is what actually fails.\n4. **Missing null checks** — use optional chaining: `item.json?.user?.email || 'fallback'`.\n5. **Webhook body nesting** — `$json.email` is undefined; use `$json.body.email`.\n6. **Auth helpers blocked** (`httpRequestWithAuthentication`) and `$env` blocked — route secrets through credentials/HTTP Request node, not the Code node sandbox.\n\n**See**: [ERROR_PATTERNS.md](ERROR_PATTERNS.md) for the comprehensive guide — each error with wrong/right code, escaping rules, the sandbox restrictions (Errors #6–#7), a prevention checklist, and a quick error-message lookup table.\n\n---\n\n## Built-in Functions & Helpers\n\n```javascript\n// HTTP requests (no auth — see sandbox note below)\nconst res = await this.helpers.httpRequest({ method: 'GET', url: 'https://api.example.com/data' });\n\n// DateTime (Luxon): now, formatting, arithmetic\nconst now = DateTime.now();\nconst formatted = now.toFormat('yyyy-MM-dd');\nconst tomorrow = now.plus({ days: 1 });\n\n// $jmespath() — query JSON structures\nconst adults = $jmespath($input.first().json, 'users[?age >= `18`]');\n\n// $getWorkflowStaticData() — data that persists across executions\n```\n\n**Sandbox (since n8n v2.0, JsTaskRunnerSandbox):** the accessor is `this.helpers.httpRequest()` — the bare `$helpers` global is **undefined** here (`$helpers.httpRequest()` throws `ReferenceError`). Inside a nested async function where `this` is lost, call it as `await fn.call(this, ...)`. `this.helpers.httpRequestWithAuthentication` and `this.helpers.requestWithAuthenticationPaginated` are deny-listed (→ `UnsupportedFunctionError`); for authenticated calls use an **HTTP Request node** with the credential (preferred), a sub-workflow, or a manual `Authorization: Bearer ${token}` header on `this.helpers.httpRequest()` only when the token already flows through the workflow as data. `$env` is blocked when `N8N_BLOCK_ENV_ACCESS_IN_NODE=true`; `require()` works only for allowlisted modules. `Buffer`, `URL`, and standard JS globals (Math, JSON, Object, Array) always work.\n\n**See**: [BUILTIN_FUNCTIONS.md](BUILTIN_FUNCTIONS.md) for the complete reference — full httpRequest options, all DateTime/Luxon operations, JMESPath patterns, static-data use cases, and the sandbox-restriction details.\n\n---\n\n## Best Practices\n\n- **Validate input first** — guard for empty arrays / missing `.json` before processing.\n- **Try-catch risky work** (HTTP calls) and return an error object instead of crashing.\n- **Prefer array methods** (`filter`/`map`/`reduce`) over manual loops.\n- **Filter early, transform late** — shrink the dataset before expensive work.\n- **Descriptive names** and `console.log()` for debugging (output goes to the browser console).\n\n**See**: [COMMON_PATTERNS.md](COMMON_PATTERNS.md) → \"Best Practices\" for code examples of each.\n\n---\n\n## Production Gotchas\n\nHard-won lessons from real deployments — summarized here, with code in [DATA_ACCESS.md](DATA_ACCESS.md) → \"Production Gotchas\":\n\n- **SplitInBatches outputs are counterintuitive**: `main[0]` = **done** (fires once, after all batches), `main[1]` = **each batch** (the loop body). Add a **Limit 1** node after the done output as a safety.\n- **Iteration count is the cost**: each loop iteration re-runs the whole body through the engine (~0.8 ms overhead each). `batchSize: 1` is the loop equivalent of *Each Item* — use the largest batch your real constraint (rate limit, page size, memory) allows, or don't loop at all.\n- **Cross-iteration accumulation (CRITICAL)**: after the loop, `$('Node Inside Loop').all()` returns ONLY the last iteration's items. Accumulate via `$getWorkflowStaticData('global')` (reset before, push inside, read after).\n- **pairedItem**: when emitting items that don't map 1:1 to input, set `pairedItem: { item: i }` or downstream Set nodes fail with `paired_item_no_info`.\n- **Node reference syntax**: `$('Node').first().json` or `$('Node').all()` — never `.json` directly on the reference.\n- **Float precision**: compare currency at the cent level — `Math.round(a*100) !== Math.round(b*100)` — to avoid false positives from float noise.\n\n---\n\n## When to Use Code Node\n\n> **Before reaching for a Code node, walk the transform gatekeeper** in the n8n Expression Syntax skill: expression → arrow-function IIFE inside an Edit Fields field → Code node, in that order. The first two paths cover most \"transform this data\" tasks at ~1–10ms each, versus the Code node's sandboxed ~500–1000ms — a ~100x gap on pure single-item shaping, with no functional difference. The Code node earns its place only for whole-dataset aggregation (`$input.all()`), allowlisted libraries, or async work. And before writing code for crypto (HMAC, hashing, signing) or XML/SOAP/RSS parsing, check for a **native node** — n8n has a Crypto node (`nodes-base.crypto`) and an XML node (`nodes-base.xml`) that cover those without any JavaScript. Dropping into Code for something a native node already does is one of the most common false positives.\n\nUse Code node when:\n- ✅ Complex transformations requiring multiple steps\n- ✅ Custom calculations or business logic\n- ✅ Recursive operations\n- ✅ API response parsing with complex structure\n- ✅ Multi-step conditionals\n- ✅ Data aggregation across items\n\nConsider other nodes when:\n- ❌ Simple field mapping → Use **Set** node\n- ❌ Basic filtering → Use **Filter** node\n- ❌ Simple conditionals → Use **IF** or **Switch** node\n- ❌ HTTP requests only → Use **HTTP Request** node\n\n**Code node excels at**: Complex logic that would require chaining many simple nodes\n\n---\n\n## Integration with Other Skills\n\n### Works With:\n\n**n8n Expression Syntax**:\n- Expressions use `{{ }}` syntax in other nodes\n- Code nodes use JavaScript directly (no `{{ }}`)\n- When to use expressions vs code\n\n**n8n MCP Tools Expert**:\n- How to find Code node: `search_nodes({query: \"code\"})`\n- Get configuration help: `get_node({nodeType: \"nodes-base.code\"})`\n- Validate code: `validate_node({nodeType: \"nodes-base.code\", config: {...}})`\n\n**n8n Node Configuration**:\n- Mode selection (All Items vs Each Item)\n- Language selection (JavaScript vs Python)\n- Understanding property dependencies\n\n**n8n Workflow Patterns**:\n- Code nodes in transformation step\n- Webhook → Code → API pattern\n- Error handling in workflows\n\n**n8n Validation Expert**:\n- Validate Code node configuration\n- Handle validation errors\n- Auto-fix common issues\n\n---\n\n## Quick Reference Checklist\n\nBefore deploying Code nodes, verify:\n\n- [ ] **Code is not empty** - Must have meaningful logic\n- [ ] **Return statement exists** - Returns items, not a primitive/`null`\n- [ ] **Canonical return format** - Each item: `{json: {...}}` (bare objects auto-wrap, but be explicit)\n- [ ] **Data access correct** - Using `$input.all()`, `$input.first()`, or `$input.item`\n- [ ] **No `{{ }}` written as code** - Use JavaScript template literals: `` `${value}` ``\n- [ ] **Error handling** - Guard clauses for null/undefined inputs\n- [ ] **Webhook data** - Access via `.body` if from webhook\n- [ ] **Mode selection** - \"All Items\" for most cases\n- [ ] **Performance** - Prefer map/filter over manual loops\n- [ ] **Output consistent** - All code paths return same structure\n\n---\n\n## Additional Resources\n\n### Related Files\n- [DATA_ACCESS.md](DATA_ACCESS.md) - Comprehensive data access patterns\n- [COMMON_PATTERNS.md](COMMON_PATTERNS.md) - 10 production-tested patterns\n- [ERROR_PATTERNS.md](ERROR_PATTERNS.md) - Top 5 errors and solutions\n- [BUILTIN_FUNCTIONS.md](BUILTIN_FUNCTIONS.md) - Complete built-in reference\n\n### n8n Documentation\n- Code Node Guide: https://docs.n8n.io/code/code-node/\n- Built-in Methods: https://docs.n8n.io/code-examples/methods-variables-reference/\n- Luxon Documentation: https://moment.github.io/luxon/\n\n---\n\n**Ready to write JavaScript in n8n Code nodes!** Start with simple transformations, use the error patterns guide to avoid common mistakes, and reference the pattern library for production-ready examples.\n","frontmatter":{"name":"n8n-code-javascript","description":"Write JavaScript code in n8n Code nodes. Use when writing JavaScript in n8n, using $input/$json/$node syntax, making HTTP requests with this.helpers / the $helpers global, working with dates using DateTime, troubleshooting Code node errors, choosing between Code node modes, or doing any custom data transformation in n8n. Always use this skill when a workflow needs a Code node — whether for data aggregation, filtering, API calls, format conversion, batch processing logic, or any custom JavaScript. Covers SplitInBatches loop patterns, cross-iteration data, pairedItem, and real-world production patterns. Also use when asked why a Code node or workflow is slow, which execution mode is faster, or how to cut per-item overhead on large datasets. EXCEPTION — for the AI-agent-callable Custom Code Tool (@n8n/n8n-nodes-langchain.toolCode, a tool attached to an AI Agent), use the n8n-code-tool skill instead; it has a different runtime contract."},"isInternal":false,"tokens":4821,"sizeBytes":19799},{"name":"COMMON_PATTERNS.md","path":"data/skills/n8n-code-python/COMMON_PATTERNS.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-code-python/COMMON_PATTERNS.md","title":"N8n-code-python Skill","category":"anthropic-skill","format":"markdown","content":"# N8n-code-python Skill\n\nPath: `data/skills/n8n-code-python/COMMON_PATTERNS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-code-python/COMMON_PATTERNS.md)","tokens":300,"sizeBytes":216},{"name":"DATA_ACCESS.md","path":"data/skills/n8n-code-python/DATA_ACCESS.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-code-python/DATA_ACCESS.md","title":"N8n-code-python Skill","category":"anthropic-skill","format":"markdown","content":"# N8n-code-python Skill\n\nPath: `data/skills/n8n-code-python/DATA_ACCESS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-code-python/DATA_ACCESS.md)","tokens":300,"sizeBytes":208},{"name":"ERROR_PATTERNS.md","path":"data/skills/n8n-code-python/ERROR_PATTERNS.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-code-python/ERROR_PATTERNS.md","title":"N8n-code-python Skill","category":"anthropic-skill","format":"markdown","content":"# N8n-code-python Skill\n\nPath: `data/skills/n8n-code-python/ERROR_PATTERNS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-code-python/ERROR_PATTERNS.md)","tokens":300,"sizeBytes":214},{"name":"README.md","path":"data/skills/n8n-code-python/README.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-code-python/README.md","title":"N8n-code-python Skill","category":"anthropic-skill","format":"markdown","content":"# N8n-code-python Skill\n\nPath: `data/skills/n8n-code-python/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-code-python/README.md)","tokens":300,"sizeBytes":198},{"name":"SKILL.md","path":"data/skills/n8n-code-python/SKILL.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-code-python/SKILL.md","title":"n8n-code-python","category":"anthropic-skill","format":"markdown","content":"---\nname: n8n-code-python\ndescription: Write Python code in n8n Code nodes. Use when writing Python in n8n, using _input/_json/_node syntax, working with standard library, or need to understand Python limitations in n8n Code nodes. Use this skill when the user specifically requests Python for an n8n Code node. Note — JavaScript is recommended for 95% of use cases — only use Python when the user explicitly prefers it or the task requires Python-specific standard library capabilities (regex, hashlib, statistics). EXCEPTION — for Python in the AI-agent-callable Custom Code Tool (@n8n/n8n-nodes-langchain.toolCode), use the n8n-code-tool skill instead (input is _query, return must be a string).\n---\n\n# Python Code Node (Beta)\n\nExpert guidance for writing Python code in n8n Code nodes.\n\n---\n\n## ⚠️ Important: JavaScript First\n\n**Recommendation**: Use **JavaScript for 95% of use cases**. Only use Python when:\n- You need specific Python standard library functions\n- You're significantly more comfortable with Python syntax\n- You're doing data transformations better suited to Python\n\n**Why JavaScript is preferred:**\n- Full n8n helper functions (`this.helpers.httpRequest`, etc.)\n- Luxon DateTime library for advanced date/time operations\n- No external library limitations\n- Better n8n documentation and community support\n\n---\n\n## Quick Start\n\n```python\n# Basic template for Python Code nodes\nitems = _input.all()\n\n# Process data\nprocessed = []\nfor item in items:\n    processed.append({\n        \"json\": {\n            **item[\"json\"],\n            \"processed\": True,\n            \"timestamp\": datetime.now().isoformat()\n        }\n    })\n\nreturn processed\n```\n\n### Essential Rules\n\n1. **Consider JavaScript first** - Use Python only when necessary\n2. **Access data**: `_input.all()`, `_input.first()`, or `_input.item`\n3. **CRITICAL**: Must return `[{\"json\": {...}}]` format\n4. **CRITICAL**: Webhook data is under `_json[\"body\"]` (not `_json` directly)\n5. **CRITICAL LIMITATION**: **No external libraries** (no requests, pandas, numpy)\n6. **Standard library only**: json, datetime, re, base64, hashlib, urllib.parse, math, random, statistics\n\n---\n\n## Mode Selection Guide\n\nSame as JavaScript - choose based on your use case:\n\n### Run Once for All Items (Recommended - Default)\n\n**Use this mode for:** 95% of use cases\n\n- **How it works**: Code executes **once** regardless of input count\n- **Data access**: `_input.all()` or `_items` array (Native mode)\n- **Best for**: Aggregation, filtering, batch processing, transformations\n- **Performance**: Faster for multiple items (single execution)\n\n```python\n# Example: Calculate total from all items\nall_items = _input.all()\ntotal = sum(item[\"json\"].get(\"amount\", 0) for item in all_items)\n\nreturn [{\n    \"json\": {\n        \"total\": total,\n        \"count\": len(all_items),\n        \"average\": total / len(all_items) if all_items else 0\n    }\n}]\n```\n\n### Run Once for Each Item\n\n**Use this mode for:** Specialized cases only\n\n- **How it works**: Code executes **separately** for each input item\n- **Data access**: `_input.item` or `_item` (Native mode)\n- **Best for**: Item-specific logic, independent operations, per-item validation\n- **Performance**: Slower for large datasets (multiple executions)\n\n```python\n# Example: Add processing timestamp to each item\nitem = _input.item\n\nreturn [{\n    \"json\": {\n        **item[\"json\"],\n        \"processed\": True,\n        \"processed_at\": datetime.now().isoformat()\n    }\n}]\n```\n\n---\n\n## Python Modes: Beta vs Native\n\nn8n offers two Python execution modes:\n\n### Python (Beta) - Recommended\n- **Use**: `_input`, `_json`, `_node` helper syntax\n- **Best for**: Most Python use cases\n- **Helpers available**: `_now`, `_today`, `_jmespath()`\n- **Import**: `from datetime import datetime`\n\n```python\n# Python (Beta) example\nitems = _input.all()\nnow = _now  # Built-in datetime object\n\nreturn [{\n    \"json\": {\n        \"count\": len(items),\n        \"timestamp\": now.isoformat()\n    }\n}]\n```\n\n### Python (Native) (Beta)\n- **Use**: `_items`, `_item` variables only\n- **No helpers**: No `_input`, `_now`, etc.\n- **More limited**: Standard Python only\n- **Use when**: Need pure Python without n8n helpers\n\n```python\n# Python (Native) example\nprocessed = []\n\nfor item in _items:\n    processed.append({\n        \"json\": {\n            \"id\": item[\"json\"].get(\"id\"),\n            \"processed\": True\n        }\n    })\n\nreturn processed\n```\n\n**Recommendation**: Use **Python (Beta)** for better n8n integration.\n\n---\n\n## Data Access Patterns\n\nAccess input data through underscore-prefixed variables. Each item is a dict shaped `{\"json\": {...}}`, so the actual fields live under `[\"json\"]`.\n\n```python\n# Pattern 1: _input.all() - Most common. Arrays, batch ops, aggregations\nall_items = _input.all()            # list of {\"json\": {...}} dicts\n\n# Pattern 2: _input.first() - Very common. Single objects, API responses\ndata = _input.first()[\"json\"]       # built-in safety vs all_items[0]\n\n# Pattern 3: _input.item - \"Run Once for Each Item\" mode ONLY\ncurrent = _input.item[\"json\"]       # None/error in All Items mode\n\n# Pattern 4: _node - Reference a specific named node\nwebhook_data = _node[\"Webhook\"][\"json\"]\nhttp_data = _node[\"HTTP Request\"][\"json\"]\n```\n\n**See**: [DATA_ACCESS.md](DATA_ACCESS.md) for the comprehensive guide — six `_input.all()` recipes (filter, transform, aggregate, sort, group, deduplicate), `_input.first()` and `_input.item` examples, multi-node combining, the JS-vs-Python variable table, and the decision tree.\n\n---\n\n## Critical: Webhook Data Structure\n\n**MOST COMMON MISTAKE**: Webhook data is nested under `[\"body\"]`\n\n```python\n# ❌ WRONG - Will raise KeyError\nname = _json[\"name\"]\nemail = _json[\"email\"]\n\n# ✅ CORRECT - Webhook data is under [\"body\"]\nname = _json[\"body\"][\"name\"]\nemail = _json[\"body\"][\"email\"]\n\n# ✅ SAFER - Use .get() for safe access\nwebhook_data = _json.get(\"body\", {})\nname = webhook_data.get(\"name\")\n```\n\n**Why**: Webhook node wraps all request data under `body` property. This includes POST data, query parameters, and JSON payloads.\n\n**See**: [DATA_ACCESS.md](DATA_ACCESS.md) for full webhook structure details\n\n---\n\n## Return Format Requirements\n\n**CRITICAL RULE**: Always return list of dictionaries with `\"json\"` key\n\n### Correct Return Formats\n\n```python\n# ✅ Single result\nreturn [{\n    \"json\": {\n        \"field1\": value1,\n        \"field2\": value2\n    }\n}]\n\n# ✅ Multiple results\nreturn [\n    {\"json\": {\"id\": 1, \"data\": \"first\"}},\n    {\"json\": {\"id\": 2, \"data\": \"second\"}}\n]\n\n# ✅ List comprehension\ntransformed = [\n    {\"json\": {\"id\": item[\"json\"][\"id\"], \"processed\": True}}\n    for item in _input.all()\n    if item[\"json\"].get(\"valid\")\n]\nreturn transformed\n\n# ✅ Empty result (when no data to return)\nreturn []\n\n# ✅ Conditional return\nif should_process:\n    return [{\"json\": processed_data}]\nelse:\n    return []\n```\n\n### Incorrect Return Formats\n\n```python\n# ❌ WRONG: Dictionary without list wrapper\nreturn {\n    \"json\": {\"field\": value}\n}\n\n# ❌ WRONG: List without json wrapper\nreturn [{\"field\": value}]\n\n# ❌ WRONG: Plain string\nreturn \"processed\"\n\n# ❌ WRONG: Incomplete structure\nreturn [{\"data\": value}]  # Should be {\"json\": value}\n```\n\n**Why it matters**: Next nodes expect list format. Incorrect format causes workflow execution to fail.\n\n**See**: [ERROR_PATTERNS.md](ERROR_PATTERNS.md) #2 for detailed error solutions\n\n---\n\n## Critical Limitation: No External Libraries\n\n**MOST IMPORTANT PYTHON LIMITATION**: Cannot import external packages on default installs.\n\n> **Self-hosted exception**: external package availability depends entirely on the instance's Python runner configuration. If the user states their self-hosted instance has specific packages available in the Python runner environment, use them — don't refuse. When unsure, ask or write standard-library-only code.\n\n**❌ NOT available** (raise `ModuleNotFoundError`): `requests`, `pandas`, `numpy`, `scipy`, `bs4`/BeautifulSoup, `lxml`.\n\n**✅ Available** (standard library only): `json`, `datetime`, `re`, `base64`, `hashlib`, `urllib.parse`, `math`, `random`, `statistics`.\n\n### Workarounds\n\n**Need HTTP requests?**\n- ✅ Use **HTTP Request node** before Code node\n- ✅ Or switch to **JavaScript** and use `this.helpers.httpRequest()` (the bare `$helpers` global is undefined in the task-runner sandbox)\n\n**Need data analysis (pandas/numpy)?**\n- ✅ Use Python **statistics** module for basic stats\n- ✅ Or switch to **JavaScript** for most operations\n- ✅ Manual calculations with lists and dictionaries\n\n**Need web scraping (BeautifulSoup)?**\n- ✅ Use **HTTP Request node** + **HTML Extract node**\n- ✅ Or switch to **JavaScript** with regex/string methods\n\n**See**: [STANDARD_LIBRARY.md](STANDARD_LIBRARY.md) for complete reference\n\n---\n\n## Common Patterns Overview\n\nBased on production workflows, the most useful Python patterns are:\n\n1. **Data Transformation** - Transform all items with list comprehensions\n2. **Filtering & Aggregation** - Sum, filter, count with built-in functions\n3. **String Processing with Regex** - Extract patterns from text with `re`\n4. **Data Validation** - Validate and clean data, attach error lists\n5. **Statistical Analysis** - Calculate mean/median/stdev with the `statistics` module\n\nCopy-ready snippets for all five live in [COMMON_PATTERNS.md](COMMON_PATTERNS.md#quick-pattern-snippets), alongside 10 fully detailed production patterns (multi-source aggregation, markdown parsing, JSON comparison, CRM normalization, dictionary lookup, top-N filtering, and more).\n\n---\n\n## Error Prevention - Top 5 Mistakes\n\n1. **Importing external libraries** (Python-specific) → `import requests` raises `ModuleNotFoundError`. Use the HTTP Request node or JavaScript instead.\n2. **Empty code or missing return** → every path must end with `return [{\"json\": ...}]`.\n3. **Incorrect return format** → wrap in a list: `{\"json\": {...}}` becomes `[{\"json\": {...}}]`.\n4. **KeyError on dictionary access** → use `.get()`: `_json.get(\"user\", {}).get(\"name\", \"Unknown\")`.\n5. **Webhook body nesting** → read via `[\"body\"]`: `_json.get(\"body\", {}).get(\"email\", \"no-email\")`.\n\n**See**: [ERROR_PATTERNS.md](ERROR_PATTERNS.md) for the comprehensive guide — each error with wrong-vs-right code, error messages, nested-access fixes, an `AttributeError` bonus case, a prevention checklist, and a quick-fix table.\n\n---\n\n## Standard Library Reference\n\nMost useful modules: `json` (parse/generate), `datetime` (dates + `timedelta`), `re` (regex), `base64` (encode/decode), `hashlib` (hashing), `urllib.parse` (URL ops), and `statistics` (mean/median/stdev). Also available: `math`, `random`, `collections`, `itertools`, `functools`.\n\nFor a condensed cheat sheet plus full per-module examples, see [STANDARD_LIBRARY.md](STANDARD_LIBRARY.md#quick-reference-most-useful-modules).\n\n---\n\n## Best Practices\n\n### 1. Always Use .get() for Dictionary Access\n\n```python\n# ✅ SAFE: Won't crash if field missing\nvalue = item[\"json\"].get(\"field\", \"default\")\n\n# ❌ RISKY: Crashes if field doesn't exist\nvalue = item[\"json\"][\"field\"]\n```\n\n### 2. Handle None/Null Values Explicitly\n\n```python\n# ✅ GOOD: Default to 0 if None\namount = item[\"json\"].get(\"amount\") or 0\n\n# ✅ GOOD: Check for None explicitly\ntext = item[\"json\"].get(\"text\")\nif text is None:\n    text = \"\"\n```\n\n### 3. Use List Comprehensions for Filtering\n\n```python\n# ✅ PYTHONIC: List comprehension\nvalid = [item for item in items if item[\"json\"].get(\"active\")]\n\n# ❌ VERBOSE: Manual loop\nvalid = []\nfor item in items:\n    if item[\"json\"].get(\"active\"):\n        valid.append(item)\n```\n\n### 4. Return Consistent Structure\n\n```python\n# ✅ CONSISTENT: Always list with \"json\" key\nreturn [{\"json\": result}]  # Single result\nreturn results  # Multiple results (already formatted)\nreturn []  # No results\n```\n\n### 5. Debug with print() Statements\n\n```python\n# Debug statements appear in browser console (F12)\nitems = _input.all()\nprint(f\"Processing {len(items)} items\")\nprint(f\"First item: {items[0] if items else 'None'}\")\n```\n\n---\n\n## Production Gotchas\n\n### SplitInBatches Loop Semantics\n\nThe SplitInBatches node has two outputs:\n- `main[0]` = **done** — fires ONCE after all batches complete\n- `main[1]` = **each batch** — fires for every batch (the loop body)\n\nAlways add a **Limit 1** node after the done output.\n\n### Correct Node Reference Syntax\n\n```python\n# ❌ WRONG\ndata = _node['HTTP Request']['json']\n\n# ✅ CORRECT - call .first() then access json\ndata = _node['HTTP Request'].first()['json']\n```\n\n### Cross-Iteration Data Not Available in Python\n\n`$getWorkflowStaticData('global')` may not be available in Python Beta mode. If you need to accumulate data across SplitInBatches iterations, use a JavaScript Code node for the accumulation logic instead.\n\n---\n\n## When to Use Python vs JavaScript\n\n### Use Python When:\n- ✅ You need `statistics` module for statistical operations\n- ✅ You're significantly more comfortable with Python syntax\n- ✅ Your logic maps well to list comprehensions\n- ✅ You need specific standard library functions\n\n### Use JavaScript When:\n- ✅ You need HTTP requests (`this.helpers.httpRequest()`)\n- ✅ You need advanced date/time (DateTime/Luxon)\n- ✅ You want better n8n integration\n- ✅ **For 95% of use cases** (recommended)\n\n### Consider Other Nodes When:\n- ❌ Simple field mapping → Use **Set** node\n- ❌ Basic filtering → Use **Filter** node\n- ❌ Simple conditionals → Use **IF** or **Switch** node\n- ❌ HTTP requests only → Use **HTTP Request** node\n\n---\n\n## Integration with Other Skills\n\n### Works With:\n\n**n8n Expression Syntax**:\n- Expressions use `{{ }}` syntax in other nodes\n- Code nodes use Python directly (no `{{ }}`)\n- When to use expressions vs code\n\n**n8n MCP Tools Expert**:\n- How to find Code node: `search_nodes({query: \"code\"})`\n- Get configuration help: `get_node({nodeType: \"nodes-base.code\"})`\n- Validate code: `validate_node({nodeType: \"nodes-base.code\", config: {...}})`\n\n**n8n Node Configuration**:\n- Mode selection (All Items vs Each Item)\n- Language selection (Python vs JavaScript)\n- Understanding property dependencies\n\n**n8n Workflow Patterns**:\n- Code nodes in transformation step\n- When to use Python vs JavaScript in patterns\n\n**n8n Validation Expert**:\n- Validate Code node configuration\n- Handle validation errors\n- Auto-fix common issues\n\n**n8n Code JavaScript**:\n- When to use JavaScript instead\n- Comparison of JavaScript vs Python features\n- Migration from Python to JavaScript\n\n---\n\n## Quick Reference Checklist\n\nBefore deploying Python Code nodes, verify:\n\n- [ ] **Considered JavaScript first** - Using Python only when necessary\n- [ ] **Code is not empty** - Must have meaningful logic\n- [ ] **Return statement exists** - Must return list of dictionaries\n- [ ] **Proper return format** - Each item: `{\"json\": {...}}`\n- [ ] **Data access correct** - Using `_input.all()`, `_input.first()`, or `_input.item`\n- [ ] **No external imports** - Only standard library (json, datetime, re, etc.)\n- [ ] **Safe dictionary access** - Using `.get()` to avoid KeyError\n- [ ] **Webhook data** - Access via `[\"body\"]` if from webhook\n- [ ] **Mode selection** - \"All Items\" for most cases\n- [ ] **Output consistent** - All code paths return same structure\n\n---\n\n## Additional Resources\n\n### Related Files\n- [DATA_ACCESS.md](DATA_ACCESS.md) - Comprehensive Python data access patterns\n- [COMMON_PATTERNS.md](COMMON_PATTERNS.md) - 10 Python patterns for n8n\n- [ERROR_PATTERNS.md](ERROR_PATTERNS.md) - Top 5 errors and solutions\n- [STANDARD_LIBRARY.md](STANDARD_LIBRARY.md) - Complete standard library reference\n\n### n8n Documentation\n- Code Node Guide: https://docs.n8n.io/code/code-node/\n- Python in n8n: https://docs.n8n.io/code/builtin/python-modules/\n\n---\n\n**Ready to write Python in n8n Code nodes - but consider JavaScript first!** Use Python for specific needs, reference the error patterns guide to avoid common mistakes, and leverage the standard library effectively.\n","frontmatter":{"name":"n8n-code-python","description":"Write Python code in n8n Code nodes. Use when writing Python in n8n, using _input/_json/_node syntax, working with standard library, or need to understand Python limitations in n8n Code nodes. Use this skill when the user specifically requests Python for an n8n Code node. Note — JavaScript is recommended for 95% of use cases — only use Python when the user explicitly prefers it or the task requires Python-specific standard library capabilities (regex, hashlib, statistics). EXCEPTION — for Python in the AI-agent-callable Custom Code Tool (@n8n/n8n-nodes-langchain.toolCode), use the n8n-code-tool skill instead (input is _query, return must be a string)."},"isInternal":false,"tokens":3941,"sizeBytes":15957},{"name":"STANDARD_LIBRARY.md","path":"data/skills/n8n-code-python/STANDARD_LIBRARY.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-code-python/STANDARD_LIBRARY.md","title":"N8n-code-python Skill","category":"anthropic-skill","format":"markdown","content":"# N8n-code-python Skill\n\nPath: `data/skills/n8n-code-python/STANDARD_LIBRARY.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-code-python/STANDARD_LIBRARY.md)","tokens":300,"sizeBytes":218},{"name":"ERROR_PATTERNS.md","path":"data/skills/n8n-code-tool/ERROR_PATTERNS.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-code-tool/ERROR_PATTERNS.md","title":"N8n-code-tool Skill","category":"anthropic-skill","format":"markdown","content":"# N8n-code-tool Skill\n\nPath: `data/skills/n8n-code-tool/ERROR_PATTERNS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-code-tool/ERROR_PATTERNS.md)","tokens":300,"sizeBytes":208},{"name":"INPUT_SCHEMA.md","path":"data/skills/n8n-code-tool/INPUT_SCHEMA.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-code-tool/INPUT_SCHEMA.md","title":"N8n-code-tool Skill","category":"anthropic-skill","format":"markdown","content":"# N8n-code-tool Skill\n\nPath: `data/skills/n8n-code-tool/INPUT_SCHEMA.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-code-tool/INPUT_SCHEMA.md)","tokens":300,"sizeBytes":204},{"name":"README.md","path":"data/skills/n8n-code-tool/README.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-code-tool/README.md","title":"N8n-code-tool Skill","category":"anthropic-skill","format":"markdown","content":"# N8n-code-tool Skill\n\nPath: `data/skills/n8n-code-tool/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-code-tool/README.md)","tokens":300,"sizeBytes":192},{"name":"SKILL.md","path":"data/skills/n8n-code-tool/SKILL.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-code-tool/SKILL.md","title":"n8n-code-tool","category":"anthropic-skill","format":"markdown","content":"---\nname: n8n-code-tool\ndescription: \"Write JavaScript or Python for the n8n Custom Code Tool (@n8n/n8n-nodes-langchain.toolCode) — the AI-agent-callable tool, NOT the workflow Code node. Use when building a Code Tool attached to an AI Agent, writing code that an LLM will invoke, parsing the `query` input, returning a string result, defining an input schema for structured arguments (specifyInputSchema, jsonSchemaExample, DynamicStructuredTool), or troubleshooting errors like \\\"Wrong output type returned\\\", \\\"No execution data available\\\", \\\"The response property should be a string, but it is an object\\\", \\\"Cannot assign to read only property 'name'\\\", or an AI agent that refuses to call the tool. Covers the critical differences between Code node and Code Tool: return format (string vs `[{json:{...}}]`), unavailability of `$fromAI`/`$input`/`$helpers` in the Code Tool sandbox, naming rules for AI invocation, and when to use `toolWorkflow`/HTTP Request Tool instead.\"\n---\n\n# n8n Custom Code Tool\n\nExpert guidance for writing code inside `@n8n/n8n-nodes-langchain.toolCode` — the tool an AI Agent can invoke, **not** the regular workflow Code node.\n\n---\n\n## ⚠️ This is NOT the Code node\n\nThe Custom Code Tool looks like a Code node in the editor — same JavaScript editor, similar layout — but it is a **completely different node** from a different package with a **different runtime contract**.\n\n| | Code node | Custom Code Tool |\n|---|---|---|\n| **Node type** | `n8n-nodes-base.code` | `@n8n/n8n-nodes-langchain.toolCode` |\n| **Package** | `n8n-nodes-base` | `@n8n/n8n-nodes-langchain` |\n| **Invoked by** | Previous node (workflow flow) | AI Agent (LangChain) |\n| **Input** | `$input.all()` — item stream | `query` — string or object from LLM |\n| **Return** | `[{json: {...}}]` (items array) | **A string** |\n| **`$fromAI()`** | N/A | **Not available** (see Errors) |\n| **HTTP helper** | `this.helpers.httpRequest` (auth helpers blocked) | Not exposed to the tool sandbox |\n| **State** | Per-run execution data | No `getContext`, no `$getWorkflowStaticData` |\n\n**If you treat it like a Code node, it fails.** The rest of this skill covers the Code Tool's actual contract.\n\n---\n\n## Quick Start\n\n### Minimal JavaScript Code Tool\n\n```javascript\n// `query` is whatever the AI sent (a string by default)\nreturn `You asked: ${query}`;\n```\n\n### Minimal Python Code Tool\n\n```python\n# `_query` is whatever the AI sent (a string by default)\nreturn f\"You asked: {_query}\"\n```\n\n### Essential Rules\n\n1. **Return a string.** Numbers are auto-converted. Anything else throws `\"The response property should be a string, but it is an object\"`.\n2. **Input variable is fixed**: `query` (JS), `_query` (Python). You cannot rename it.\n3. **Do NOT use `$fromAI()`** inside the Code Tool sandbox — it throws `\"No execution data available\"`.\n4. **Do NOT use `[{json: {...}}]`** return format — that's for Code nodes. Throws `\"Wrong output type returned\"`.\n5. **Use a descriptive tool name** (letters/numbers/underscores, v1.1+). The agent calls the tool by its name.\n6. **Write a precise description** — the LLM decides whether to invoke the tool based on it.\n\n---\n\n## The Two Input Modes\n\nThe Code Tool has two input shapes, controlled by `specifyInputSchema`:\n\n### Mode 1: Unstructured (default, `specifyInputSchema: false`)\n\nThe AI passes **a single string** as `query`. If you need multiple fields, the AI has to stuff them into that one string and you parse them out. In practice, LLMs will happily pass a JSON string if your description tells them to.\n\n```javascript\n// Parse a JSON string the AI sent\nlet params;\ntry {\n  params = typeof query === 'string' ? JSON.parse(query) : query;\n} catch (e) {\n  throw new Error('Expected a JSON object. Parser said: ' + e.message);\n}\nconst price = Number(params.price);\nconst months = Number(params.months);\n// ...\nreturn JSON.stringify({ monthly_payment: /* ... */ });\n```\n\n**Pros**: simplest to set up, one field to describe.\n**Cons**: no schema validation — if the LLM forgets a field, the tool throws at runtime.\n\n**Best for**: quick prototypes, tools with one natural input (a question, a URL, a text blob).\n\n### Mode 2: Structured (`specifyInputSchema: true`)\n\nThe tool becomes a LangChain `DynamicStructuredTool`. The LLM sees a typed argument schema and passes a **validated object** as `query`. You access fields directly.\n\n```javascript\n// query is now an object matching your schema\nconst price = query.price;\nconst months = query.months;\nconst residual_percent = query.residual_percent;\n\nconst monthly = computeAnnuity(price, months, residual_percent);\nreturn JSON.stringify({ monthly_payment: monthly });\n```\n\nSchema is defined via either:\n- `schemaType: \"fromJson\"` + `jsonSchemaExample` (n8n v≥1.3) — paste an example JSON, n8n infers the schema\n- `schemaType: \"manual\"` + `inputSchema` — write a full JSON Schema yourself\n\n**Pros**: LLM gets type hints, invalid calls rejected before your code runs, cleaner code.\n**Cons**: a little more setup; requires n8n version with schema support.\n\n**Best for**: production tools with multiple typed parameters (calculators, API wrappers, anything with numeric fields the LLM tends to stringify).\n\n**See**: [INPUT_SCHEMA.md](INPUT_SCHEMA.md) for complete schema setup.\n\n---\n\n## Return Format\n\n**The return value must be a string.** The LLM reads it as the tool's observation.\n\n```javascript\n// ✅ String\nreturn \"42\";\n\n// ✅ Number (auto-converted to string by n8n)\nreturn 42;\n\n// ✅ JSON-encoded structured result (recommended for rich output)\nreturn JSON.stringify({ result: 42, currency: \"SEK\" });\n\n// ❌ Raw object → \"The response property should be a string, but it is an object\"\nreturn { result: 42 };\n\n// ❌ Workflow item format → \"Wrong output type returned\"\nreturn [{ json: { result: 42 } }];\n\n// ❌ Array → \"The response property should be a string, but it is an object\"\nreturn [1, 2, 3];\n```\n\n### Best practice: JSON-stringify structured results\n\nWhen your tool has more than a trivial scalar output, return a JSON string:\n\n```javascript\nreturn JSON.stringify({\n  monthly_payment_sek: 5405,\n  loan_amount: 351920,\n  total_cost_of_credit: 63295\n});\n```\n\nThe LLM parses JSON reliably and can pick the fields it needs to present to the user.\n\n### Error handling: the agent reads your failures\n\nErrors don't just stop the workflow — they go back to the LLM, which usually corrects its call and retries. Use that:\n\n```javascript\n// Option A: throw — n8n surfaces the message to the agent\nif (!isFinite(price)) throw new Error('price must be a number, e.g. 439900');\n\n// Option B: return an error string — agent reads it like any tool result\nif (!isFinite(price)) return JSON.stringify({ error: 'price must be a number, e.g. 439900' });\n```\n\nEither way, write error messages **for the LLM**: state what was wrong and what a valid call looks like. A bare `throw new Error('invalid input')` wastes the retry; an instructive message usually fixes the next call.\n\n---\n\n## Tool Name and Description\n\nThese fields are NOT documentation — they are the **tool contract the LLM sees**. Treat them as prompt engineering.\n\n### Name\n- Must match `[A-Za-z0-9_]+` (v1.1+). No spaces, no hyphens, no emoji.\n- Use a verb-y descriptive name: `calculate_car_loan`, `get_weather`, `search_orders`.\n- The agent calls the tool by this name. `Code Tool` (the default) is useless — the agent won't know when to call it.\n\n### Description\n- Explain **when** to use it and **what** to send.\n- If unstructured mode, **include an example of the JSON string** the LLM should send.\n- If structured mode, the schema speaks for itself — just describe purpose.\n\n**Unstructured example (JSON-in-string pattern):**\n```\nDeterministiskt beräknar månadskostnad för billån. Anropa med EN JSON-sträng:\n{\"price\":439900,\"down_payment\":87980,\"interest_rate\":6.95,\"months\":36,\"residual_percent\":50}\nFält: price (SEK), down_payment (SEK), interest_rate (% per år), months, residual_percent (0-99).\n```\n\n**Structured example (schema-defined):**\n```\nDeterministically computes the monthly car-loan payment given price, down payment, \nannual interest rate, term, and residual percent. Use whenever the user asks for \nmonthly cost, total credit cost, or loan breakdown.\n```\n\n---\n\n## Top Errors and Fixes\n\n### Error 1: `\"There was an error: 'Cannot assign to read only property \\\"name\\\" of object: Error: No execution data available'\"`\n\n**Cause**: you called `$fromAI()` inside the Code Tool sandbox.\n\n**Fix**: `$fromAI()` is a helper for **other** tool-enabled nodes (HTTP Request Tool, SendGrid Tool, `toolWorkflow`, etc.) — it's not exposed inside `toolCode`. Read the AI's input from `query` directly (or use `specifyInputSchema` for structured fields).\n\n### Error 2: `\"Wrong output type returned\"`\n\n**Cause**: you returned a workflow-style array like `[{ json: { ... } }]`. That's the Code **node** contract, not the Code **Tool** contract.\n\n**Fix**: return a string. For structured data, `return JSON.stringify(output)`.\n\n### Error 3: `\"The response property should be a string, but it is an object\"`\n\n**Cause**: you returned a plain object or array.\n\n**Fix**: `JSON.stringify()` the result, or coerce to a string.\n\n### Error 4: AI never calls the tool\n\n**Cause**: tool name is generic (`Code Tool`, `My Tool`) or description doesn't clearly state when to use it.\n\n**Fix**: rename to a verb-y name (`calculate_car_loan`), and rewrite the description to explicitly state the trigger conditions (e.g. \"Use this whenever the user asks about monthly cost\").\n\n### Error 5: AI sends garbage into `query`\n\n**Cause**: unstructured tool with a vague description. The LLM guesses at the format.\n\n**Fix**: either (a) include a concrete JSON example in the description, or (b) switch to `specifyInputSchema: true` so the LLM gets a typed schema.\n\n**See**: [ERROR_PATTERNS.md](ERROR_PATTERNS.md) for full catalog with reproductions.\n\n---\n\n## What's NOT Available in the Sandbox\n\nThe Code Tool sandbox is **narrower** than the Code node sandbox. Don't assume helpers carry over:\n\n| Helper | Code node | Code Tool |\n|---|---|---|\n| `$input.all()`, `$input.first()`, `$input.item` | ✅ | ❌ |\n| `$node[\"NodeName\"]` | ✅ | ❌ |\n| `$json`, `$binary` | ✅ | ❌ |\n| `$fromAI()` | ❌ | ❌ (despite sitting next to an AI agent) |\n| `this.helpers.httpRequest()` | ✅ | ❌ |\n| `DateTime` (Luxon) | ✅ | ✅ (standard in JS sandbox) |\n| `$jmespath()` | ✅ | ❌ |\n| `this.getContext(...)` | ✅ | ❌ |\n| `$getWorkflowStaticData(...)` | ✅ | ❌ |\n\n**Implication**: the Code Tool is for **pure computation**. If you need an HTTP call, an API lookup, or cross-invocation state, use a different tool node:\n- HTTP Request Tool for external API calls\n- `toolWorkflow` (Call Sub-workflow Tool) for multi-step logic with access to the full Code node sandbox\n- MCP / database tools for persistent state\n\n---\n\n## When to Use Code Tool vs Alternatives\n\nUse **Code Tool** when:\n- ✅ Pure deterministic computation (math, parsing, formatting, validation)\n- ✅ Lightweight transformations the LLM shouldn't do itself (precision math, regex)\n- ✅ You want the code inline in the workflow, not in a separate sub-workflow\n\nUse **`toolWorkflow`** (Call Sub-workflow Tool) when:\n- ✅ You need multiple parameters with clean `$fromAI()` typing\n- ✅ You need access to `this.helpers`, credentials, or other nodes\n- ✅ Logic is reusable across agents\n- ✅ You want structured typed inputs WITHOUT writing a JSON Schema\n\nUse **HTTP Request Tool** when:\n- ✅ The tool is fundamentally a single API call\n- ✅ You want per-parameter `$fromAI()` bindings in URL/query/body\n\n**Rule of thumb**: if you find yourself wanting `$fromAI()`, you probably want `toolWorkflow` instead of `toolCode`.\n\n---\n\n## Complete Working Example\n\nA production calculator tool (unstructured, JSON-in-string pattern):\n\n```json\n{\n  \"parameters\": {\n    \"name\": \"calculate_car_loan\",\n    \"description\": \"Computes monthly car-loan payment using an annuity formula with residual/balloon. Call with a single JSON string. Example: {\\\"price\\\":439900,\\\"down_payment\\\":87980,\\\"interest_rate\\\":6.95,\\\"months\\\":36,\\\"residual_percent\\\":50,\\\"setup_fee\\\":695,\\\"monthly_admin_fee\\\":59}. Required: price, down_payment, interest_rate, months, residual_percent. Optional: setup_fee, monthly_admin_fee (default 0).\",\n    \"language\": \"javaScript\",\n    \"jsCode\": \"let params;\\ntry {\\n  params = typeof query === 'string' ? JSON.parse(query) : query;\\n} catch (e) {\\n  throw new Error('Invalid JSON: ' + e.message);\\n}\\n\\nconst price           = Number(params.price);\\nconst down_payment    = Number(params.down_payment);\\nconst interest_rate   = Number(params.interest_rate);\\nconst months          = Number(params.months);\\nconst residual_percent= Number(params.residual_percent);\\nconst setup_fee       = Number(params.setup_fee ?? 0) || 0;\\nconst monthly_admin_fee = Number(params.monthly_admin_fee ?? 0) || 0;\\n\\nif (!isFinite(price) || price <= 0) throw new Error('price must be > 0');\\nif (down_payment < 0 || down_payment >= price) throw new Error('down_payment must be in [0, price)');\\n\\nconst principal = price - down_payment;\\nconst residual  = price * (residual_percent / 100);\\nconst r = interest_rate / 100 / 12;\\nconst growth = Math.pow(1 + r, months);\\nconst base = r === 0\\n  ? (principal - residual) / months\\n  : (principal - residual / growth) * r / (1 - 1 / growth);\\nconst monthly_payment = base + monthly_admin_fee;\\n\\nreturn JSON.stringify({\\n  monthly_payment_sek: Math.round(monthly_payment),\\n  loan_amount: Math.round(principal),\\n  residual_value_sek: Math.round(residual),\\n  total_cost_of_credit: Math.round(monthly_payment * months + residual + setup_fee - principal)\\n});\"\n  },\n  \"type\": \"@n8n/n8n-nodes-langchain.toolCode\",\n  \"typeVersion\": 1.3,\n  \"name\": \"calculate_car_loan\"\n}\n```\n\nWire it into an AI Agent via the `ai_tool` connection type.\n\n---\n\n## Integration with Other Skills\n\n**n8n-code-javascript**: the Code **node** skill. Most JavaScript patterns (arrays, map/filter, DateTime) transfer — but I/O contract is different. Don't copy data-access code.\n\n**n8n-node-configuration**: `specifyInputSchema` is a classic displayOptions-driven conditional field. Use `get_node({detail: \"standard\"})` on `@n8n/n8n-nodes-langchain.toolCode` to see schema-related properties.\n\n**n8n-workflow-patterns**: Code Tool sits inside the \"AI Agent with tools\" pattern. An agent typically has several tools; Code Tool is the \"local compute\" option.\n\n**n8n-validation-expert**: the three Code Tool errors listed above have clear signatures — if validation surfaces \"Wrong output type returned\", you know to switch from array-of-items to a string.\n\n---\n\n## Quick Reference Checklist\n\nBefore deploying a Code Tool:\n\n- [ ] **Node type** is `@n8n/n8n-nodes-langchain.toolCode` (not `nodes-base.code`)\n- [ ] **Tool name** is descriptive, verb-y, snake_case (e.g. `calculate_car_loan`)\n- [ ] **Description** states when to use the tool and (if unstructured) shows a JSON example\n- [ ] **Input** read from `query` (JS) or `_query` (Python)\n- [ ] **No `$fromAI()`** in the code body\n- [ ] **No `$input` / `$json` / `$helpers`** — those aren't in the sandbox\n- [ ] **Return** is a string (use `JSON.stringify()` for structured output)\n- [ ] **Wired** into an AI Agent via `ai_tool` connection\n- [ ] **Tested** with the exact kind of input the LLM will send (JSON in a string, or schema-validated object)\n\n---\n\n## Additional Resources\n\n- [INPUT_SCHEMA.md](INPUT_SCHEMA.md) — structured input (DynamicStructuredTool) in depth\n- [ERROR_PATTERNS.md](ERROR_PATTERNS.md) — full error catalog with causes and fixes\n\n### Official sources\n- [n8n Custom Code Tool docs](https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.toolcode/)\n- [ToolCode source](https://github.com/n8n-io/n8n/blob/master/packages/%40n8n/nodes-langchain/nodes/tools/ToolCode/ToolCode.node.ts) — the sandbox contract\n- [LangChain tool docs](https://js.langchain.com/docs/modules/agents/tools/) — DynamicTool / DynamicStructuredTool\n\n---\n\n**Remember**: the Code Tool is a LangChain tool wearing a Code-node UI. Contract is: **string in, string out**. Everything else follows from that.\n","frontmatter":{"name":"n8n-code-tool","description":"Write JavaScript or Python for the n8n Custom Code Tool (@n8n/n8n-nodes-langchain.toolCode) — the AI-agent-callable tool, NOT the workflow Code node. Use when building a Code Tool attached to an AI Agent, writing code that an LLM will invoke, parsing the `query` input, returning a string result, defining an input schema for structured arguments (specifyInputSchema, jsonSchemaExample, DynamicStructuredTool), or troubleshooting errors like \"Wrong output type returned\", \"No execution data available\", \"The response property should be a string, but it is an object\", \"Cannot assign to read only property 'name'\", or an AI agent that refuses to call the tool. Covers the critical differences between Code node and Code Tool: return format (string vs `[{json:{...}}]`), unavailability of `$fromAI`/`$input`/`$helpers` in the Code Tool sandbox, naming rules for AI invocation, and when to use `toolWorkflow`/HTTP Request Tool instead."},"isInternal":false,"tokens":4134,"sizeBytes":16247},{"name":"API_WORKFLOWS.md","path":"data/skills/n8n-error-handling/API_WORKFLOWS.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-error-handling/API_WORKFLOWS.md","title":"N8n-error-handling Skill","category":"anthropic-skill","format":"markdown","content":"# N8n-error-handling Skill\n\nPath: `data/skills/n8n-error-handling/API_WORKFLOWS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-error-handling/API_WORKFLOWS.md)","tokens":300,"sizeBytes":221},{"name":"ERROR_WORKFLOWS.md","path":"data/skills/n8n-error-handling/ERROR_WORKFLOWS.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-error-handling/ERROR_WORKFLOWS.md","title":"N8n-error-handling Skill","category":"anthropic-skill","format":"markdown","content":"# N8n-error-handling Skill\n\nPath: `data/skills/n8n-error-handling/ERROR_WORKFLOWS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-error-handling/ERROR_WORKFLOWS.md)","tokens":300,"sizeBytes":225},{"name":"NODE_ERROR_OUTPUTS.md","path":"data/skills/n8n-error-handling/NODE_ERROR_OUTPUTS.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-error-handling/NODE_ERROR_OUTPUTS.md","title":"N8n-error-handling Skill","category":"anthropic-skill","format":"markdown","content":"# N8n-error-handling Skill\n\nPath: `data/skills/n8n-error-handling/NODE_ERROR_OUTPUTS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-error-handling/NODE_ERROR_OUTPUTS.md)","tokens":300,"sizeBytes":231},{"name":"README.md","path":"data/skills/n8n-error-handling/README.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-error-handling/README.md","title":"N8n-error-handling Skill","category":"anthropic-skill","format":"markdown","content":"# N8n-error-handling Skill\n\nPath: `data/skills/n8n-error-handling/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-error-handling/README.md)","tokens":300,"sizeBytes":207},{"name":"RESPONSE_SHAPES.md","path":"data/skills/n8n-error-handling/RESPONSE_SHAPES.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-error-handling/RESPONSE_SHAPES.md","title":"N8n-error-handling Skill","category":"anthropic-skill","format":"markdown","content":"# N8n-error-handling Skill\n\nPath: `data/skills/n8n-error-handling/RESPONSE_SHAPES.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-error-handling/RESPONSE_SHAPES.md)","tokens":300,"sizeBytes":225},{"name":"SKILL.md","path":"data/skills/n8n-error-handling/SKILL.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-error-handling/SKILL.md","title":"n8n-error-handling","category":"anthropic-skill","format":"markdown","content":"---\nname: n8n-error-handling\ndescription: Wire n8n error handling so failures are loud, structured, and recoverable. Use when building any webhook/API workflow, a scheduled or unattended workflow, or any path where a silent failure would drop user-visible work — and whenever the user mentions error handling, onError, continueErrorOutput, error branches/outputs, retries, retryOnFail, Respond to Webhook status codes, 4xx/5xx, Error Trigger, or \"my workflow fails silently\". Covers per-node error outputs and wiring, retry/self-healing, error-trigger workflows, and 4xx/5xx response shapes.\n---\n\n# n8n Error Handling\n\nBy default, when an n8n node throws, the **whole workflow halts**. For an interactive run you're watching, that's fine — you see the red node and fix it. For anything unattended (a webhook API, a cron job, a queue worker, an agent tool), it's the wrong default: the caller gets a timeout or an empty 500, the operator gets no alert, and the symptom is \"the integration just stopped working\" with no log and no clue.\n\nThis skill is about making failures **loud, structured, and recoverable** — and, best case, **self-healing** so transient blips never reach a human at all.\n\nThe two ideas that prevent most silent failures:\n\n- **Per-node error outputs** — a node's failure routes down a second output you control, instead of killing the run.\n- **A workflow-level error workflow** — a catch-all that fires for anything that escapes per-node handling (timeouts, crashes between nodes, unwired failures).\n\n---\n\n## When you actually need this\n\n| Workflow shape | Error handling posture |\n|---|---|\n| Webhook / API (anything with `Respond to Webhook`) | **Required.** Every fallible node's error output wired; status code matches cause. |\n| Scheduled / cron / queue worker / agent tool (unattended) | **Required.** A workflow-level error workflow, plus `retryOnFail` on network nodes. |\n| Internal one-off you run and watch yourself | **Optional.** Default `onError: \"stopWorkflow\"` is fine — you'll see the red node and re-run. |\n\nThe dividing line: **if anyone other than you sees the output** — a downstream system, an end user, an on-call engineer — the failure has to be handled, not swallowed. If you're the only watcher and the cost of failure is \"I notice and re-run\", looser is fine.\n\n---\n\n## The #1 silent trap: per-node error output is a TWO-step setup\n\nThis is the single most common way an n8n workflow \"handles\" errors while actually swallowing them. Routing a node's failure to a handler takes **two** changes, and doing only one looks complete but misbehaves:\n\n1. **Set `onError: \"continueErrorOutput\"`** on the node. This is what *creates* the second output. Without it, `main[1]` doesn't exist no matter what you wire.\n2. **Wire that error output** (`connections.<node>.main[1]`, i.e. `sourceIndex: 1`) to a real handler. Without a target, the error data is emitted into the void.\n\nGet one without the other and you hit a failure mode:\n\n| What you did | What happens at runtime |\n|---|---|\n| `onError` set, error output **not** wired | Error data is silently discarded. Downstream doesn't fire. The dashboard shows the run as **succeeded**. Worst case — no error logged anywhere. |\n| Error output wired, `onError` **not** set | The slot never fires; the handler is unreachable. On failure the workflow just **halts** (default `stopWorkflow`). |\n| Both done | Failure routes down `main[1]` to your handler. ✅ |\n\n### Doing both with `n8n_update_partial_workflow`\n\n```javascript\n// 1) Turn on the error output (creates main[1])\n{ type: \"updateNode\", nodeName: \"HTTP Request\",\n  changes: { onError: \"continueErrorOutput\" } }\n\n// 2) Wire the error output to a handler. sourceIndex: 1 = the error output.\n{ type: \"addConnection\",\n  source: \"HTTP Request\",\n  target: \"Handle Error\",\n  sourceIndex: 1 }\n```\n\n`sourceIndex: 0` is the success path, `sourceIndex: 1` is the error path. (For IF nodes the aliases `branch: \"true\"`/`\"false\"` map to index 0/1; for a generic fallible node, use the explicit `sourceIndex: 1`.)\n\n**Then verify.** This trap doesn't surface in `validate_workflow` — a half-wired error output validates clean. Pull the workflow with `n8n_get_workflow` and confirm **both** halves:\n\n- The node's `onError` is `\"continueErrorOutput\"`.\n- `connections[\"HTTP Request\"].main[1]` contains your handler.\n\nValid `onError` values:\n\n| Value | Effect |\n|---|---|\n| `\"stopWorkflow\"` (default) | Error halts the whole workflow. |\n| `\"continueRegularOutput\"` | Error item flows out the **normal** output. Rare, usually wrong — downstream gets error-shaped data and keeps going. |\n| `\"continueErrorOutput\"` | Error item flows out the **separate** error output (`main[1]`). The one you wire. |\n\nFull failure-mode catalog, fan-in/fan-out shapes, and verification: **NODE_ERROR_OUTPUTS.md**.\n\n---\n\n## Self-healing first: `retryOnFail` before you wire error paths\n\nBefore you build error branches, absorb the transient failures so they never reach those branches. On **any node that calls a network service** — HTTP Request, comms (Gmail/Slack/Discord), databases, AI nodes, third-party integrations — set node-level retry:\n\n```javascript\n{ type: \"updateNode\", nodeName: \"HTTP Request\",\n  changes: {\n    retryOnFail: true,\n    maxTries: 3,\n    waitBetweenTries: 5000   // ms\n  } }\n```\n\nWhy this comes **first**: a 429 or a brief upstream hiccup will retry and usually succeed on its own. The error output then fires only on *real, persistent* failures — so your 5xx responses and on-call alerts reflect actual problems instead of noise.\n\nEngine limits to know: retry fires on **any** error (there's no per-status-code filter), `maxTries` caps at 5, and `waitBetweenTries` caps at 5000ms — so 5000 is both the max and a sensible default. See **n8n-node-configuration** (NODE_FAMILY_GOTCHAS.md) for node-specific notes.\n\n---\n\n## API workflows: the canonical shape\n\nA webhook-triggered workflow that responds to its caller has one rule that overrides everything else: **no hanging branches**. Every path — success and every error — must end at a `Respond to Webhook`, or the caller sits there until it times out.\n\n```\nWebhook (responseMode: \"responseNode\")\n  ├── validate input → process → Respond (200, body)\n  └── (any fallible node's error output → sourceIndex 1)\n            → Respond (4xx/5xx, structured error body)\n            → optional: log full error privately / notify\n```\n\nThree things make this work:\n\n1. **Fan-in to one error responder.** Many fallible nodes can route their `main[1]` to a single `Respond` node. Keeps the graph readable.\n2. **Validation failures (4xx) are checked *upstream*, not via error outputs.** A missing field isn't a node *crashing* — it's an expected outcome with a known response. Branch on it with IF/Switch (or the schema validator below) and return 400/401/403/404 directly. Error outputs are for *unexpected* failures (5xx).\n3. **`responseCode` defaults to 200 — even on error branches.** This is its own silent trap (see RESPONSE_SHAPES.md and **n8n-node-configuration** NODE_FAMILY_GOTCHAS.md): an error branch that returns 200 with an error body looks like success to the caller's HTTP client, so their error handling never fires. Set `responseCode` explicitly on every Respond node.\n\n### Input validation: the Set-node schema validator\n\nFor any endpoint doing structured input validation, run the check as an IIFE inside a single **Set** node rather than a chain of IF/Switch nodes per field. One node validates the whole payload, returns `{ valid, validationError, details, requiredSchema }`, and an IF branches on `valid` → your logic (200) or a 400 Respond that echoes the schema back so the caller can self-correct. It's also dramatically faster than a recursive validator in a Code node + sub-workflow. The full pattern, the constraint cookbook, and the expression-escaping gotchas live in **API_WORKFLOWS.md**.\n\n---\n\n## Response shapes: map cause → status code\n\nA 5xx with `text/plain \"Internal Server Error\"` is technically an error response and practically useless. And not every failure is a 5xx. **Match the status code to *why* the request failed**, because the caller branches on it: their monitoring alerts on 5xx (your fault) but not 4xx (their fault), and 5xx suggests \"retry\" while 4xx suggests \"don't\".\n\n**The common mistake:** wiring everything — including bad input — to one `Respond` that returns 500 `internal_error`. Now the caller can't tell their bug from your outage, and your error rates can't separate real incidents from client noise.\n\n| Cause | Status | `error` code | Where it's handled |\n|---|---|---|---|\n| Required field missing / wrong type | 400 | `validation_error` | Upstream check (schema validator / IF), not error output |\n| Auth missing or invalid | 401 | `unauthorized` | Upstream check |\n| Authenticated but not allowed | 403 | `forbidden` | Upstream check |\n| Resource ID valid in request, absent in your data | 404 | `not_found` | Branch on the lookup *result*, not its error |\n| Conflicts with current state (duplicate, race) | 409 | `conflict` | Detect with logic |\n| Caller exceeded rate limit | 429 | `rate_limit_exceeded` | Set `Retry-After` header |\n| Node threw, cause unknown | 500 | `internal_error` | Error output path |\n| Third-party API returned an error | 502 | `upstream_error` | Error output of the HTTP node |\n| Can't process right now (downstream down) | 503 | `service_unavailable` | Detect specific error, hint retry |\n| Third-party API timed out | 504 | `upstream_timeout` | Error output filtered by message |\n\nSo there are two distinct flows: **4xx is decided before the work** (IF/Switch + dedicated Respond), **5xx comes out of error outputs** (\"we tried, it broke\").\n\n**One Respond, expression-driven code.** When error paths differ only by *number and message* (same body shape, same headers), don't fan out to N Respond nodes through a Switch. The Respond node accepts expressions in both `Response Code` and body — compute the code inline:\n\n```javascript\n// Response Code field on a single Respond to Webhook:\n{{ (() => {\n    const msg = $json.error?.message || $json.message || '';\n    if (msg.includes('INVALID_ID')) return 400;\n    if (/429|too many/i.test(msg)) return 429;\n    if (/timeout/i.test(msg))      return 504;\n    if (/upstream|llm|api/i.test(msg)) return 502;\n    return 500;\n})() }}\n```\n\nReserve Switch + multiple Responds for paths that diverge *structurally* (different headers, different body shapes, redirects). Same shape with a different number is one expression-driven Respond.\n\nThe default envelope is `{ \"error\": \"<code>\", \"message\": \"<human text>\" }` — the HTTP status already says success-vs-failure, so no `ok: false` flag. **Never leak internals** (stack traces, SQL, upstream bodies, tokens) into the response — log those privately, return a sanitized message. Correlation IDs, `retry_after`, validation `details`, and the full do-not-leak list are in **RESPONSE_SHAPES.md**.\n\n---\n\n## Workflow-level error workflow (the catch-all)\n\nPer-node outputs handle the failures you anticipated on the nodes you remembered to wire. An **error workflow** catches everything else: a node you forgot to wire, a crash between nodes, a whole-workflow timeout, a trigger failure. For unattended workflows this is the safety net that turns \"it silently stopped\" into \"an alert arrived\".\n\nBuild it as a separate workflow starting with an **Error Trigger** node. n8n invokes it with the failure context:\n\n```json\n{\n  \"execution\": { \"id\": \"...\", \"url\": \"...\", \"lastNodeExecuted\": \"Fetch order\",\n    \"error\": { \"name\": \"NodeApiError\", \"message\": \"...\", \"timestamp\": 1715000000000 } },\n  \"workflow\": { \"id\": \"...\", \"name\": \"Sync Stripe customers\" }\n}\n```\n\nMinimal version — **capture → notify**:\n\n```\nError Trigger → Set (build alert from execution + error) → Slack/email (post to #incidents)\n```\n\nA good alert includes the workflow name, a link to the editor and a link to the failed execution, the failed node name, and the **real** error message (not \"Workflow failed\"). Field expressions and the optional \"fetch the failing input via the n8n node\" upgrade are in **ERROR_WORKFLOWS.md**.\n\nTwo traps worth flagging up front:\n\n- **The recursion trap.** If the error workflow notifies Slack and Slack is what's down, the error workflow fails too — and the original error vanishes. Notify on a *different* channel than your monitored workflows use (most workflows alert Slack → error workflow uses email), and add a fallback (write to a Data Table) so a failed notification still leaves a trace.\n- **A \"handled\" error won't bubble up.** If a node's error output is wired to a no-op that drops the data, n8n considers the error *handled* and the error workflow does **not** fire. Only catch per-node when you're actually doing something with the error.\n\n> **What the community MCP can't do:** assigning the error workflow (instance default or per-workflow override) is an n8n **UI setting** — Workflow Settings → Error Workflow. There is no MCP tool to set it. Build the error workflow with the MCP, then tell the user the exact UI step to wire it up, and to repeat it (or set the instance default) for every unattended workflow.\n\n---\n\n## What's NOT available via the community MCP\n\n| Want to do | Reality |\n|---|---|\n| Set a workflow's **Error Workflow** setting | UI only (Workflow Settings → Error Workflow). No MCP tool. Build the workflow, then hand the user the UI step. |\n| Toggle other **workflow settings** (Save Execution Data, timezone, timeout, caller policy) | UI only. `n8n_update_partial_workflow` has `updateSettings`, but the error-workflow assignment is not reliably exposed — confirm in the UI. |\n| Enable instance-wide error logging (Sentry, server logs) | Instance config, outside n8n workflows entirely. |\n\nWhat the MCP **can** do: build the error workflow, set `onError`/`retryOnFail` on nodes (`updateNode`/`patchNodeField`), wire error outputs (`addConnection` with `sourceIndex: 1`), validate (`validate_workflow`, `n8n_validate_workflow`), auto-fix common issues (`n8n_autofix_workflow`), test (`n8n_test_workflow`), and inspect failures (`n8n_executions`).\n\n---\n\n## Anti-patterns\n\n| Anti-pattern | What goes wrong | Fix |\n|---|---|---|\n| `onError` set but error output unwired | Error silently discarded; run shows as **succeeded** | Wire `sourceIndex: 1` to a real handler, or revert `onError` to `stopWorkflow` so it's loud |\n| Error output wired but `onError` not set | Slot never fires; handler unreachable; workflow halts on failure | Set `onError: \"continueErrorOutput\"` |\n| Webhook → process → respond, no error branch | Caller gets a timeout or n8n's generic 500 | Wire every fallible node's error output to a Respond |\n| Error branch returns 200 with an `{error}` body | Caller's client reads success; their error handling never fires | Set `responseCode` to 4xx/5xx explicitly on error Responds |\n| One 500 `internal_error` for everything | Caller can't tell their bad input from your outage | Map cause → status (4xx caller, 5xx you) |\n| Catching errors in a Code node and returning them as data | Downstream processes error-shaped data and continues | Let it throw; use `onError: \"continueErrorOutput\"` + wired path |\n| Network node with no `retryOnFail` | Every transient 429/blip surfaces as a 5xx; alerts fire on noise | `retryOnFail: true, maxTries: 3, waitBetweenTries: 5000` |\n| Switch → N Responds differing only by status code | 5 nodes for what's one Respond | Compute the code inline in one expression-driven Respond |\n| Unattended workflow with no error workflow | A genuine failure goes nowhere | Build an Error Trigger workflow + assign it in the UI |\n| Error workflow notifies the same channel the workflows monitor | Channel down → error workflow also fails → error vanishes | Use a different channel + a Data Table fallback |\n| Leaking `$json.error` (stack/SQL/tokens) into the response | Exposes internals to callers/attackers | Log privately, return a sanitized message |\n\n---\n\n## Reference files\n\n| File | Read when |\n|---|---|\n| **NODE_ERROR_OUTPUTS.md** | Wiring a per-node error output on individual fallible nodes |\n| **API_WORKFLOWS.md** | Building/reviewing a webhook → Respond workflow, including the schema validator |\n| **RESPONSE_SHAPES.md** | Defining response body conventions, status codes, and what not to leak |\n| **ERROR_WORKFLOWS.md** | Setting up the workflow-level catch-all for unattended workflows |\n\n---\n\n## Integration with other skills\n\n- **n8n-workflow-patterns** — the webhook/API and scheduled patterns are where error handling lives. Use it for the overall shape; use this skill to harden it.\n- **n8n-node-configuration** — `onError`/`retryOnFail` are node config; NODE_FAMILY_GOTCHAS.md covers the Webhook/Respond response-code traps in depth.\n- **n8n-validation-expert** — the half-wired error output (one of the two steps missing) is a connection/config audit item, not a validation error. This skill is the fix.\n- **n8n-expression-syntax** — the expression-driven `Response Code` and the alert-message expressions rely on correct `{{ }}` syntax and `$json.error` access.\n- **n8n-code-javascript / n8n-code-python** — if you catch errors *inside* a Code node, decide deliberately: re-throw to use the error output, or handle and continue. Don't return error-shaped data and pretend it succeeded.\n- **n8n-code-tool** — an agent's Code Tool surfaces thrown errors back to the LLM, which then retries; that's a different error contract from workflow nodes.\n- **n8n-binary-and-data** — file/binary operations are fallible too; wire their error outputs like any network node.\n\n---\n\n## Quick reference checklist\n\nFor an **API / webhook** workflow:\n\n- [ ] Webhook trigger uses `responseMode: \"responseNode\"`\n- [ ] Input validated upstream → 4xx Respond (schema validator or IF)\n- [ ] Every fallible node has `onError: \"continueErrorOutput\"` **and** `main[1]` wired\n- [ ] Network nodes have `retryOnFail: true, maxTries: 3, waitBetweenTries: 5000`\n- [ ] Error path ends at a Respond with an **explicit** 4xx/5xx `responseCode`\n- [ ] Status code matches cause (4xx caller, 5xx you)\n- [ ] Error body is `{ error, message }` — no stack traces, SQL, or tokens\n- [ ] Verified with `n8n_get_workflow`: both `onError` and `main[1]` present on each fallible node\n\nFor an **unattended** (scheduled/cron/queue) workflow:\n\n- [ ] Network nodes have `retryOnFail` configured\n- [ ] An Error Trigger workflow exists (capture → notify, optional retry)\n- [ ] The error workflow notifies on a different channel + has a fallback (recursion trap)\n- [ ] The error-workflow setting is assigned in the n8n UI (MCP can't do it — remind the user)\n\n---\n\n**Remember**: the default is silence. Error handling is two moves — make the failure *route* (per-node `onError` + wired output, or a catch-all error workflow) and make it *speak* (a status code and body that tell the truth). Half a move is worse than none, because it looks done.\n","frontmatter":{"name":"n8n-error-handling","description":"Wire n8n error handling so failures are loud, structured, and recoverable. Use when building any webhook/API workflow, a scheduled or unattended workflow, or any path where a silent failure would drop user-visible work — and whenever the user mentions error handling, onError, continueErrorOutput, error branches/outputs, retries, retryOnFail, Respond to Webhook status codes, 4xx/5xx, Error Trigger, or \"my workflow fails silently\". Covers per-node error outputs and wiring, retry/self-healing, error-trigger workflows, and 4xx/5xx response shapes."},"isInternal":false,"tokens":4657,"sizeBytes":19074},{"name":"COMMON_MISTAKES.md","path":"data/skills/n8n-expression-syntax/COMMON_MISTAKES.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-expression-syntax/COMMON_MISTAKES.md","title":"N8n-expression-syntax Skill","category":"anthropic-skill","format":"markdown","content":"# N8n-expression-syntax Skill\n\nPath: `data/skills/n8n-expression-syntax/COMMON_MISTAKES.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-expression-syntax/COMMON_MISTAKES.md)","tokens":300,"sizeBytes":234},{"name":"EXAMPLES.md","path":"data/skills/n8n-expression-syntax/EXAMPLES.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-expression-syntax/EXAMPLES.md","title":"N8n-expression-syntax Skill","category":"anthropic-skill","format":"markdown","content":"# N8n-expression-syntax Skill\n\nPath: `data/skills/n8n-expression-syntax/EXAMPLES.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-expression-syntax/EXAMPLES.md)","tokens":300,"sizeBytes":220},{"name":"README.md","path":"data/skills/n8n-expression-syntax/README.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-expression-syntax/README.md","title":"N8n-expression-syntax Skill","category":"anthropic-skill","format":"markdown","content":"# N8n-expression-syntax Skill\n\nPath: `data/skills/n8n-expression-syntax/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-expression-syntax/README.md)","tokens":300,"sizeBytes":216},{"name":"SKILL.md","path":"data/skills/n8n-expression-syntax/SKILL.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-expression-syntax/SKILL.md","title":"n8n-expression-syntax","category":"anthropic-skill","format":"markdown","content":"---\nname: n8n-expression-syntax\ndescription: Validate n8n expression syntax and fix common errors. Use when writing n8n expressions, using {{}} syntax, accessing $json/$node variables, troubleshooting expression errors, mapping data between nodes, or referencing webhook data in workflows. Use this skill whenever configuring node fields that reference data from previous nodes — expressions are how n8n passes data between nodes, and getting the syntax wrong is the most common source of workflow errors. Also use when asked whether a complex expression hurts performance.\n---\n\n# n8n Expression Syntax\n\nExpert guide for writing correct n8n expressions in workflows.\n\n---\n\n## Expression Format\n\nAll dynamic content in n8n uses **double curly braces**:\n\n```\n{{expression}}\n```\n\n**Examples**:\n```\n✅ {{$json.email}}\n✅ {{$json.body.name}}\n✅ {{$node[\"HTTP Request\"].json.data}}\n❌ $json.email  (no braces - treated as literal text)\n❌ {$json.email}  (single braces - invalid)\n```\n\n---\n\n## Core Variables\n\n### $json - Current Node Output\n\nAccess data from the current node:\n\n```javascript\n{{$json.fieldName}}\n{{$json['field with spaces']}}\n{{$json.nested.property}}\n{{$json.items[0].name}}\n```\n\n### $node - Reference Other Nodes\n\nAccess data from any previous node:\n\n```javascript\n{{$node[\"Node Name\"].json.fieldName}}\n{{$node[\"HTTP Request\"].json.data}}\n{{$node[\"Webhook\"].json.body.email}}\n```\n\n**Important**:\n- Node names **must** be in quotes\n- Node names are **case-sensitive**\n- Must match exact node name from workflow\n\n### $now - Current Timestamp\n\nAccess current date/time:\n\n```javascript\n{{$now}}\n{{$now.toFormat('yyyy-MM-dd')}}\n{{$now.toFormat('HH:mm:ss')}}\n{{$now.plus({days: 7})}}\n```\n\n### $env - Environment Variables\n\nAccess environment variables:\n\n```javascript\n{{$env.API_KEY}}\n{{$env.DATABASE_URL}}\n```\n\n**Warning**: Some n8n instances have `N8N_BLOCK_ENV_ACCESS_IN_NODE` enabled, which blocks `$env` access entirely. If `$env` returns errors, use alternative approaches:\n- Store values in credentials instead\n- Use a Set node with manually entered values\n- Pass values through webhook query parameters\n\n---\n\n## 🚨 CRITICAL: Webhook Data Structure\n\n**Most Common Mistake**: Webhook data is **NOT** at the root!\n\n### Webhook Node Output Structure\n\n```javascript\n{\n  \"headers\": {...},\n  \"params\": {...},\n  \"query\": {...},\n  \"body\": {           // ⚠️ USER DATA IS HERE!\n    \"name\": \"John\",\n    \"email\": \"john@example.com\",\n    \"message\": \"Hello\"\n  }\n}\n```\n\n### Correct Webhook Data Access\n\n```javascript\n❌ WRONG: {{$json.name}}\n❌ WRONG: {{$json.email}}\n\n✅ CORRECT: {{$json.body.name}}\n✅ CORRECT: {{$json.body.email}}\n✅ CORRECT: {{$json.body.message}}\n```\n\n**Why**: Webhook node wraps incoming data under `.body` property to preserve headers, params, and query parameters.\n\n---\n\n## Common Patterns\n\n### Access Nested Fields\n\n```javascript\n// Simple nesting\n{{$json.user.email}}\n\n// Array access\n{{$json.data[0].name}}\n{{$json.items[0].id}}\n\n// Bracket notation for spaces\n{{$json['field name']}}\n{{$json['user data']['first name']}}\n```\n\n### Reference Other Nodes\n\n```javascript\n// Node without spaces\n{{$node[\"Set\"].json.value}}\n\n// Node with spaces (common!)\n{{$node[\"HTTP Request\"].json.data}}\n{{$node[\"Respond to Webhook\"].json.message}}\n\n// Webhook node\n{{$node[\"Webhook\"].json.body.email}}\n```\n\n### Combine Variables\n\n```javascript\n// Concatenation (automatic)\nHello {{$json.body.name}}!\n\n// In URLs\nhttps://api.example.com/users/{{$json.body.user_id}}\n\n// In object properties\n{\n  \"name\": \"={{$json.body.name}}\",\n  \"email\": \"={{$json.body.email}}\"\n}\n```\n\n---\n\n## When NOT to Use Expressions\n\n### ❌ Code Nodes\n\nCode nodes use **direct JavaScript access**, NOT expressions!\n\n```javascript\n// ❌ WRONG in Code node\nconst email = '={{$json.email}}';\nconst name = '{{$json.body.name}}';\n\n// ✅ CORRECT in Code node\nconst email = $json.email;\nconst name = $json.body.name;\n\n// Or using Code node API\nconst email = $input.item.json.email;\nconst allItems = $input.all();\n```\n\n### ❌ Webhook Paths\n\n```javascript\n// ❌ WRONG\npath: \"{{$json.user_id}}/webhook\"\n\n// ✅ CORRECT\npath: \"user-webhook\"  // Static paths only\n```\n\n### ❌ Credential Fields\n\n```javascript\n// ❌ WRONG\napiKey: \"={{$env.API_KEY}}\"\n\n// ✅ CORRECT\nUse n8n credential system, not expressions\n```\n\n---\n\n## The transform gatekeeper\n\nBefore you add any node — or write any code — to transform data, walk this order and stop at the first that fits:\n\n1. **Expression** (`{{ ... }}`) in the consuming field. Property access, method chains (`.map().filter().join()`), ternaries, string building, Luxon date math — if it's \"take A, produce B\" without intermediate variables, it's an expression. This covers most \"just transform this\" cases.\n2. **Arrow-function IIFE inside an Edit Fields field.** When the logic needs intermediate variables, branching, or comments but still operates on one item, wrap it in an immediately-invoked arrow function right in the field value:\n\n   ```\n   ={{ (() => {\n       const items = $json.line_items;\n       const subtotal = items.reduce((sum, it) => sum + it.price * it.qty, 0);\n       const tax = subtotal * 0.08;\n       return (subtotal + tax).toFixed(2);\n   })() }}\n   ```\n\n   The outer `(...)` brackets the function; the trailing `()` invokes it. Drop either and n8n refuses to run. Inside you get the full expression scope (`$json`, `$('Node')`, `$now`, Luxon) plus `const`/`let`, `if`/`switch`, `try`/`catch`, and regex. No `require`, no `await`.\n3. **Code node — last resort.** Only when you need multi-item aggregation across the whole dataset (`$input.all()`), an allowlisted library, or async work.\n\n**Why the order matters.** It's not style — it's readability and performance. The Code node runs in a sandboxed VM with per-invocation setup and value marshaling — a cold-start cost that can reach 500–1000ms before your logic runs. (It amortizes on warm, high-item-count runs, so treat this as the common-case cost, not a universal constant.) The same logic in an expression or Edit Fields IIFE runs in-process in single-digit milliseconds and skips the sandbox entirely. For pure single-item shaping that's a large gap with no functional difference, and it compounds on hot paths like per-request webhooks. The expression also stays visible in the field that uses it, instead of hiding in an upstream node someone has to open to understand. Reach past a stage only when the input or scope genuinely demands it.\n\n## The Set-node antipattern and branch convergence\n\n### Delete Set nodes that feed one consumer\n\nA Set / Edit Fields node whose only job is to extract a value and hand it to **one** downstream node is dead weight. Inline its expression at the consumer instead.\n\n```\n❌  Webhook → Set { customer_id: {{ $json.body.customer_id }} } → Postgres: WHERE id = {{ $json.customer_id }}\n\n✅  Webhook → Postgres: WHERE id = {{ $('Webhook').item.json.body.customer_id }}\n```\n\nThe Set node adds a hop, more canvas clutter, and a refactor hazard, while doing nothing the consumer couldn't do itself. To remove it cleanly with `n8n_update_partial_workflow`: rewire the connection (`removeConnection` from the Set's source-and-target, `addConnection` straight from source to consumer), `patchNodeField` the consumer's expression to reference the original source by node name, then `removeNode` the Set.\n\n**Quick test:** count how many downstream nodes reference each field the Set produces.\n- **0 or 1** → delete, inline at the consumer.\n- **2+** → it may earn its place.\n\n**Legitimate exceptions** — keep the Set when:\n- **2+ consumers** read the same derived value and the derivation is non-trivial (a name aids readability and you compute it once).\n- **It's a sub-workflow's final Return node**, shaping the output contract. Here the \"single consumer\" is every caller, so the Set *is* the API boundary — and with `Include Other Fields: false` it whitelists the output shape so internal scratch fields don't leak.\n- **You're renaming or whitelisting fields** and want that visible in one place rather than spread across consumer expressions.\n\n### Branch convergence: anchor with a NoOp\n\nWhen branches converge (after IF/Switch/Merge), `$json` becomes \"whichever branch fired last\" — non-deterministic, and a silent source of wrong data. Insert a **NoOp** node at the convergence, name it descriptively (`Combine Inputs`), and have downstream nodes reference it by name:\n\n```\nBranch A ──┐\n           ├─→ [NoOp: Combine Inputs] ──→ downstream uses $('Combine Inputs').item.json.x\nBranch B ──┘\n```\n\nThe NoOp survives refactors: inserting a transform later between it and the consumer doesn't break the `$('Combine Inputs')` reference. (If the branches produce *different* shapes, use a Set node instead of a NoOp to normalize both into one shape — see the exceptions above.)\n\nMore broadly in branchy flows, **prefer `$('Node').item.json.x` over deep `$json.x`.** `$json` breaks the moment an intermediate node is inserted or a node clears item context (Aggregate, Code with Run for All, branching merges); the failure is silent and downstream gets the wrong data with no error. A node-name reference is unambiguous regardless of what sits between source and consumer.\n\n---\n\n## Validation Rules\n\n### 1. Always Use {{}}\n\nExpressions **must** be wrapped in double curly braces.\n\n```javascript\n❌ $json.field\n✅ {{$json.field}}\n```\n\n### 2. Use Quotes for Spaces and Special Characters\n\nField or node names with spaces, diacritics, or special characters require **bracket notation**:\n\n```javascript\n❌ {{$json.field name}}\n✅ {{$json['field name']}}\n\n❌ {{$node.HTTP Request.json}}\n✅ {{$node[\"HTTP Request\"].json}}\n\n// Bracket notation is mandatory for keys with special characters\n✅ {{$json['Gross Price w/o shipment']}}\n✅ {{$json['Cena brutto zł']}}\n```\n\n### 3. Match Exact Node Names\n\nNode references are **case-sensitive**:\n\n```javascript\n❌ {{$node[\"http request\"].json}}  // lowercase\n❌ {{$node[\"Http Request\"].json}}  // wrong case\n✅ {{$node[\"HTTP Request\"].json}}  // exact match\n```\n\n### 4. No Nested {{}}\n\nDon't double-wrap expressions:\n\n```javascript\n❌ {{{$json.field}}}\n✅ {{$json.field}}\n```\n\n---\n\n## Common Mistakes\n\nFor complete error catalog with fixes, see [COMMON_MISTAKES.md](COMMON_MISTAKES.md)\n\n### Quick Fixes\n\n| Mistake | Fix |\n|---------|-----|\n| `$json.field` | `{{$json.field}}` |\n| `{{$json.field name}}` | `{{$json['field name']}}` |\n| `{{$node.HTTP Request}}` | `{{$node[\"HTTP Request\"]}}` |\n| `{{{$json.field}}}` | `{{$json.field}}` |\n| `{{$json.name}}` (webhook) | `{{$json.body.name}}` |\n| `'={{$json.email}}'` (Code node) | `$json.email` |\n\n---\n\n## Working Examples\n\nFor real workflow examples, see [EXAMPLES.md](EXAMPLES.md)\n\n### Example 1: Webhook to Slack\n\n**Webhook receives**:\n```json\n{\n  \"body\": {\n    \"name\": \"John Doe\",\n    \"email\": \"john@example.com\",\n    \"message\": \"Hello!\"\n  }\n}\n```\n\n**In Slack node text field**:\n```\nNew form submission!\n\nName: {{$json.body.name}}\nEmail: {{$json.body.email}}\nMessage: {{$json.body.message}}\n```\n\n### Example 2: HTTP Request to Email\n\n**HTTP Request returns**:\n```json\n{\n  \"data\": {\n    \"items\": [\n      {\"name\": \"Product 1\", \"price\": 29.99}\n    ]\n  }\n}\n```\n\n**In Email node** (reference HTTP Request):\n```\nProduct: {{$node[\"HTTP Request\"].json.data.items[0].name}}\nPrice: ${{$node[\"HTTP Request\"].json.data.items[0].price}}\n```\n\n### Example 3: Format Timestamp\n\n```javascript\n// Current date\n{{$now.toFormat('yyyy-MM-dd')}}\n// Result: 2025-10-20\n\n// Time\n{{$now.toFormat('HH:mm:ss')}}\n// Result: 14:30:45\n\n// Full datetime\n{{$now.toFormat('yyyy-MM-dd HH:mm')}}\n// Result: 2025-10-20 14:30\n```\n\n---\n\n## Data Type Handling\n\n### Arrays\n\n```javascript\n// First item\n{{$json.users[0].email}}\n\n// Array length\n{{$json.users.length}}\n\n// Last item\n{{$json.users[$json.users.length - 1].name}}\n```\n\n### Objects\n\n```javascript\n// Dot notation (no spaces)\n{{$json.user.email}}\n\n// Bracket notation (with spaces or dynamic)\n{{$json['user data'].email}}\n```\n\n### Strings\n\n```javascript\n// Concatenation (automatic)\nHello {{$json.name}}!\n\n// String methods\n{{$json.email.toLowerCase()}}\n{{$json.name.toUpperCase()}}\n```\n\n### Numbers\n\n```javascript\n// Direct use\n{{$json.price}}\n\n// Math operations\n{{$json.price * 1.1}}  // Add 10%\n{{$json.quantity + 5}}\n```\n\n---\n\n## Advanced Patterns\n\n### Conditional Content\n\n```javascript\n// Ternary operator\n{{$json.status === 'active' ? 'Active User' : 'Inactive User'}}\n\n// Default values\n{{$json.email || 'no-email@example.com'}}\n```\n\n### Date Manipulation\n\n```javascript\n// Add days\n{{$now.plus({days: 7}).toFormat('yyyy-MM-dd')}}\n\n// Subtract hours\n{{$now.minus({hours: 24}).toISO()}}\n\n// Set specific date\n{{DateTime.fromISO('2025-12-25').toFormat('MMMM dd, yyyy')}}\n```\n\n### String Manipulation\n\n```javascript\n// Substring\n{{$json.email.substring(0, 5)}}\n\n// Replace\n{{$json.message.replace('old', 'new')}}\n\n// Split and join\n{{$json.tags.split(',').join(', ')}}\n```\n\n---\n\n## Performance: expression complexity is (almost) free\n\nA common worry is that a complex `{{ }}` is slow. It isn't — what costs is *how many times* n8n evaluates an expression, not how elaborate each one is.\n\nMeasured on an n8n 2.x instance, an elaborate expression (`sqrt`, `split`, `reduce`, arithmetic) costs the same per item as a trivial `{{ $json.x > 50 }}` — roughly **~0.2 ms/item either way**, because ~90% of that is n8n building the per-item evaluation context, not running your expression.\n\nWhat this means in practice:\n\n- **Don't break a working expression into a chain of nodes for \"speed.\"** Each extra node re-evaluates per item and re-copies all items; one node with one richer expression beats three nodes with simple ones.\n- **An expression (~0.2 ms/item) is ~3× cheaper than a Code node in \"Run Once for Each Item\" mode** (~0.6 ms/item) for the same per-item check — but a Code node in \"Run Once for All Items\" mode is cheaper still (~0.02 ms/item), because it crosses the per-item boundary once instead of N times.\n- This only bites at **thousands of items**; below that it's sub-100 ms. The **n8n Code JavaScript** skill has the full per-item-boundary model.\n\n---\n\n## Debugging Expressions\n\n### Test in Expression Editor\n\n1. Click field with expression\n2. Open expression editor (click \"fx\" icon)\n3. See live preview of result\n4. Check for errors highlighted in red\n\n### Common Error Messages\n\n**\"Cannot read property 'X' of undefined\"**\n→ Parent object doesn't exist\n→ Check your data path\n\n**\"X is not a function\"**\n→ Trying to call method on non-function\n→ Check variable type\n\n**Expression shows as literal text**\n→ Missing {{ }}\n→ Add curly braces\n\n---\n\n## Expression Helpers\n\n### Available Methods\n\n**String**:\n- `.toLowerCase()`, `.toUpperCase()`\n- `.trim()`, `.replace()`, `.substring()`\n- `.split()`, `.includes()`\n\n**Array**:\n- `.length`, `.map()`, `.filter()`\n- `.find()`, `.join()`, `.slice()`\n\n**DateTime** (Luxon):\n- `.toFormat()`, `.toISO()`, `.toLocal()`\n- `.plus()`, `.minus()`, `.set()`\n\n**Number**:\n- `.toFixed()`, `.toString()`\n- Math operations: `+`, `-`, `*`, `/`, `%`\n\n---\n\n## Best Practices\n\n### ✅ Do\n\n- Always use {{ }} for dynamic content\n- Use bracket notation for field names with spaces\n- Reference webhook data from `.body`\n- Use $node for data from other nodes\n- Test expressions in expression editor\n\n### ❌ Don't\n\n- Don't use expressions in Code nodes\n- Don't forget quotes around node names with spaces\n- Don't double-wrap with extra {{ }}\n- Don't assume webhook data is at root (it's under .body!)\n- Don't use expressions in webhook paths or credentials\n\n---\n\n## Related Skills\n\n- **n8n MCP Tools Expert**: Learn how to validate expressions using MCP tools\n- **n8n Workflow Patterns**: See expressions in real workflow examples\n- **n8n Node Configuration**: Understand when expressions are needed\n\n---\n\n## Summary\n\n**Essential Rules**:\n1. Wrap expressions in {{ }}\n2. Webhook data is under `.body`\n3. No {{ }} in Code nodes\n4. Quote node names with spaces\n5. Node names are case-sensitive\n\n**Most Common Mistakes**:\n- Missing {{ }} → Add braces\n- `{{$json.name}}` in webhooks → Use `{{$json.body.name}}`\n- `{{$json.email}}` in Code → Use `$json.email`\n- `{{$node.HTTP Request}}` → Use `{{$node[\"HTTP Request\"]}}`\n\nFor more details, see:\n- [COMMON_MISTAKES.md](COMMON_MISTAKES.md) - Complete error catalog\n- [EXAMPLES.md](EXAMPLES.md) - Real workflow examples\n\n---\n\n**Need Help?** Reference the n8n expression documentation or use n8n-mcp validation tools to check your expressions.\n","frontmatter":{"name":"n8n-expression-syntax","description":"Validate n8n expression syntax and fix common errors. Use when writing n8n expressions, using {{}} syntax, accessing $json/$node variables, troubleshooting expression errors, mapping data between nodes, or referencing webhook data in workflows. Use this skill whenever configuring node fields that reference data from previous nodes — expressions are how n8n passes data between nodes, and getting the syntax wrong is the most common source of workflow errors. Also use when asked whether a complex expression hurts performance."},"isInternal":false,"tokens":4176,"sizeBytes":16570},{"name":"OPERATIONS_GUIDE.md","path":"data/skills/n8n-mcp-tools-expert/OPERATIONS_GUIDE.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-mcp-tools-expert/OPERATIONS_GUIDE.md","title":"N8n-mcp-tools-expert Skill","category":"anthropic-skill","format":"markdown","content":"# N8n-mcp-tools-expert Skill\n\nPath: `data/skills/n8n-mcp-tools-expert/OPERATIONS_GUIDE.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-mcp-tools-expert/OPERATIONS_GUIDE.md)","tokens":300,"sizeBytes":233},{"name":"README.md","path":"data/skills/n8n-mcp-tools-expert/README.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-mcp-tools-expert/README.md","title":"N8n-mcp-tools-expert Skill","category":"anthropic-skill","format":"markdown","content":"# N8n-mcp-tools-expert Skill\n\nPath: `data/skills/n8n-mcp-tools-expert/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-mcp-tools-expert/README.md)","tokens":300,"sizeBytes":213},{"name":"SEARCH_GUIDE.md","path":"data/skills/n8n-mcp-tools-expert/SEARCH_GUIDE.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-mcp-tools-expert/SEARCH_GUIDE.md","title":"N8n-mcp-tools-expert Skill","category":"anthropic-skill","format":"markdown","content":"# N8n-mcp-tools-expert Skill\n\nPath: `data/skills/n8n-mcp-tools-expert/SEARCH_GUIDE.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-mcp-tools-expert/SEARCH_GUIDE.md)","tokens":300,"sizeBytes":225},{"name":"SKILL.md","path":"data/skills/n8n-mcp-tools-expert/SKILL.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-mcp-tools-expert/SKILL.md","title":"n8n-mcp-tools-expert","category":"anthropic-skill","format":"markdown","content":"---\nname: n8n-mcp-tools-expert\ndescription: Expert guide for using n8n-mcp MCP tools effectively. Use when searching for nodes, validating configurations, accessing templates, managing workflows, organizing workflows into folders, managing credentials, auditing instance security, or using any n8n-mcp tool. Provides tool selection guidance, parameter formats, and common patterns. IMPORTANT — Always consult this skill before calling any n8n-mcp tool — it prevents common mistakes like wrong nodeType formats, incorrect parameter structures, and inefficient tool usage. If the user mentions n8n, workflows, nodes, or automation and you have n8n MCP tools available, use this skill first.\n---\n\n# n8n MCP Tools Expert\n\nMaster guide for using n8n-mcp MCP server tools to build workflows.\n\n---\n\n## Tool Categories\n\nn8n-mcp provides tools organized into categories:\n\n1. **Node Discovery** → [SEARCH_GUIDE.md](SEARCH_GUIDE.md)\n2. **Configuration Validation** → [VALIDATION_GUIDE.md](VALIDATION_GUIDE.md)\n3. **Workflow Management** → [WORKFLOW_GUIDE.md](WORKFLOW_GUIDE.md)\n4. **Template Library** - Search and deploy 2,700+ real workflows\n5. **Data Tables** - Manage n8n data tables, rows and columns (`n8n_manage_datatable`)\n6. **Workflow Folders** - Folder CRUD + workflow placement (`n8n_manage_folders`)\n7. **Credential Management** - Full credential CRUD + schema discovery (`n8n_manage_credentials`)\n8. **Security & Audit** - Instance security auditing with custom deep scan (`n8n_audit_instance`)\n9. **Documentation & Guides** - Tool docs, AI agent guide, Code node guides\n10. **Agents** - Create, configure, validate, run and publish persisted n8n Agents (`n8n_manage_agents`, requires `N8N_MCP_ACCESS_TOKEN`)\n11. **Node Resource Resolution** - Resolve live dropdown/resource-locator values with a real credential (`n8n_explore_node_resources`, requires `N8N_MCP_ACCESS_TOKEN`)\n12. **Instance Catalog** - List projects and tags (`n8n_list_catalog`)\n\n---\n\n## Quick Reference\n\n### Most Used Tools (by success rate)\n\n| Tool | Use When | Speed |\n|------|----------|-------|\n| `search_nodes` | Finding nodes by keyword | <20ms |\n| `get_node` | Understanding node operations (detail=\"standard\") | <10ms |\n| `validate_node` | Checking configurations (mode=\"full\") | <100ms |\n| `n8n_create_workflow` | Creating workflows | 100-500ms |\n| `n8n_update_partial_workflow` | Editing workflows (MOST USED!) | 50-200ms |\n| `validate_workflow` | Checking complete workflow | 100-500ms |\n| `n8n_deploy_template` | Deploy template to n8n instance | 200-500ms |\n| `n8n_manage_datatable` | Managing data tables and rows | 50-500ms |\n| `n8n_manage_folders` | Folder CRUD + organizing workflows | 100-500ms |\n| `n8n_manage_credentials` | Credential CRUD + schema discovery | 50-500ms |\n| `n8n_audit_instance` | Security audit (built-in + custom scan) | 500-5000ms |\n| `n8n_autofix_workflow` | Auto-fix validation errors | 200-1500ms |\n| `n8n_manage_agents` | Persisted n8n Agent CRUD/validate/publish | 150-400ms; `call` action: 5-60s |\n| `n8n_explore_node_resources` | Resolve live loadOptions/listSearch values | 200 ms - 5 s |\n| `n8n_list_catalog` | List projects or tags | 50-300ms |\n\n---\n\n## Tool Selection Guide\n\n### Finding the Right Node\n\n**Workflow**:\n```\n1. search_nodes({query: \"keyword\"})\n2. get_node({nodeType: \"nodes-base.name\"})\n3. [Optional] get_node({nodeType: \"nodes-base.name\", mode: \"docs\"})\n```\n\n**Example**:\n```javascript\n// Step 1: Search\nsearch_nodes({query: \"slack\"})\n// Returns: nodes-base.slack\n\n// Step 2: Get details\nget_node({nodeType: \"nodes-base.slack\"})\n// Returns: operations, properties, examples (standard detail)\n\n// Step 3: Get readable documentation\nget_node({nodeType: \"nodes-base.slack\", mode: \"docs\"})\n// Returns: markdown documentation\n```\n\n**Common pattern**: search → get_node (18s average)\n\n### Validating Configuration\n\n**Workflow**:\n```\n1. validate_node({nodeType, config: {}, mode: \"minimal\"}) - Check required fields\n2. validate_node({nodeType, config, profile: \"runtime\"}) - Full validation\n3. [Repeat] Fix errors, validate again\n```\n\n**Common pattern**: validate → fix → validate (23s thinking, 58s fixing per cycle)\n\n### Managing Workflows\n\n**Workflow**:\n```\n1. n8n_create_workflow({name, nodes, connections})\n2. n8n_validate_workflow({id})\n3. n8n_update_partial_workflow({id, operations: [...]})\n4. n8n_validate_workflow({id}) again\n5. n8n_update_partial_workflow({id, operations: [{type: \"activateWorkflow\"}]})\n```\n\n**Common pattern**: iterative updates (56s average between edits)\n\n### Critical: Node JSON Hygiene When Creating Workflows\n\nThree structural mistakes in generated node JSON break the n8n UI even when the workflow validates:\n\n1. **Never emit a `credentials` block with a placeholder ID.** A fake ID like `\"id\": \"REPLACE_ME\"` renders the credential selector permanently disabled and non-clickable in the n8n UI (\"No credentials yet\") — the user has to recreate the node from scratch. If you don't know the real credential ID, **omit the `credentials` block entirely**; an absent block shows a normal empty dropdown the user can click. Use `n8n_manage_credentials({action: \"list\"})` to discover real credential IDs first.\n\n```javascript\n// ❌ Breaks the credential selector\n\"credentials\": {\"httpHeaderAuth\": {\"id\": \"REPLACE_ME\", \"name\": \"My API Key\"}}\n\n// ✅ Unknown ID → omit credentials block; user picks in UI\n// ✅ Known ID (from n8n_manage_credentials list) → use the real ID\n```\n\n2. **Generate UUID v4 values for node `id`** — not human-readable strings like `\"http-list-node\"`. n8n's frontend uses node IDs for form binding and credential component initialization; non-UUID IDs cause subtle UI breakage.\n\n3. **Use the current `typeVersion`** for each node — check `get_node` rather than hardcoding remembered versions (e.g. httpRequest is at 4.4+, not 4.2).\n\n---\n\n## Critical: nodeType Formats\n\n**Two different formats** for different tools!\n\n### Format 1: Search/Validate Tools\n```javascript\n// Use SHORT prefix\n\"nodes-base.slack\"\n\"nodes-base.httpRequest\"\n\"nodes-base.webhook\"\n\"nodes-langchain.agent\"\n```\n\n**Tools that use this**:\n- search_nodes (returns this format)\n- get_node\n- validate_node\n- validate_workflow\n\n### Format 2: Workflow Tools\n```javascript\n// Use FULL prefix\n\"n8n-nodes-base.slack\"\n\"n8n-nodes-base.httpRequest\"\n\"n8n-nodes-base.webhook\"\n\"@n8n/n8n-nodes-langchain.agent\"\n```\n\n**Tools that use this**:\n- n8n_create_workflow\n- n8n_update_partial_workflow\n\n### Conversion\n\n```javascript\n// search_nodes returns BOTH formats\n{\n  \"nodeType\": \"nodes-base.slack\",          // For search/validate tools\n  \"workflowNodeType\": \"n8n-nodes-base.slack\"  // For workflow tools\n}\n```\n\n---\n\n## Common Mistakes\n\nEight recurring mistakes. Two are worth showing in full because they silently corrupt structure:\n\n```javascript\n// nodeType prefix (search/validate tools want the SHORT form)\nget_node({nodeType: \"slack\"})              // ❌ missing prefix → \"Node not found\"\nget_node({nodeType: \"n8n-nodes-base.slack\"}) // ❌ FULL prefix is for workflow tools\nget_node({nodeType: \"nodes-base.slack\"})     // ✅\n\n// credentials must be nested by type with {id, name} — not a flat string\nupdates: {credentials: \"myApiKey\"}                              // ❌\nupdates: {credentials: {httpHeaderAuth: {id: \"abc123\", name: \"My API Key\"}}}  // ✅\n```\n\n| # | Mistake | Fix |\n|---|---------|-----|\n| 1 | Wrong nodeType format | SHORT `nodes-base.*` for search/validate; FULL `n8n-nodes-base.*` for workflow tools (see above) |\n| 2 | `detail: \"full\"` by default | Default `standard` covers 95%; reach for `docs`/`search_properties` instead of `full` |\n| 3 | No validation profile | Pass `profile: \"runtime\"` explicitly (`minimal`/`ai-friendly`/`strict` for other stages) |\n| 4 | Ignoring auto-sanitization | ALL nodes sanitized on ANY update (operator structures, IF/Switch metadata); it can't fix broken connections or branch-count mismatches |\n| 5 | Not using smart parameters | Use `branch: \"true\"` / `case: 0` instead of fragile `sourceIndex` math |\n| 6 | Omitting `intent` | Always include `intent` on `n8n_update_partial_workflow` for better responses |\n| 7 | `parameters` instead of `updates` | `updateNode` takes `updates: {...}`, not `parameters: {...}` |\n| 8 | Wrong credential format | Nest by type with `{id, name}` (see above) |\n\nFull WRONG/CORRECT examples for each: see [VALIDATION_GUIDE.md → Common Mistakes](VALIDATION_GUIDE.md).\n\n---\n\n## Tool Usage Patterns\n\nThree patterns dominate real usage. Worked, step-by-step examples for each live in the reference guides.\n\n- **Pattern 1 — Node Discovery** (18s avg between steps): `search_nodes({query})` → `get_node({nodeType, includeExamples: true})`. See [SEARCH_GUIDE.md](SEARCH_GUIDE.md).\n- **Pattern 2 — Validation Loop** (23s thinking, 58s fixing): `validate_node({profile: \"runtime\"})` → read `errors` → fix config → validate again until clean. See [VALIDATION_GUIDE.md](VALIDATION_GUIDE.md).\n- **Pattern 3 — Workflow Editing** (99.0% success, 56s avg between edits): iterate `n8n_update_partial_workflow` (with `intent`) → `n8n_validate_workflow` → finally `activateWorkflow`. Build iteratively, NOT one-shot. See [WORKFLOW_GUIDE.md](WORKFLOW_GUIDE.md).\n\n---\n\n## Detailed Guides\n\n### Node Discovery Tools\nSee [SEARCH_GUIDE.md](SEARCH_GUIDE.md) for:\n- search_nodes\n- get_node with detail levels (minimal, standard, full)\n- get_node modes (info, docs, search_properties, versions)\n\n### Validation Tools\nSee [VALIDATION_GUIDE.md](VALIDATION_GUIDE.md) for:\n- Validation profiles explained\n- validate_node with modes (minimal, full)\n- validate_workflow complete structure\n- Auto-sanitization system\n- Handling validation errors\n\n### Workflow Management\nSee [WORKFLOW_GUIDE.md](WORKFLOW_GUIDE.md) for:\n- n8n_create_workflow\n- n8n_update_partial_workflow (21 operation types including patchNodeField, setNodeGroups, and moveToFolder!)\n- Smart parameters (branch, case)\n- AI connection types (8 types)\n- Workflow activation (activateWorkflow/deactivateWorkflow)\n- n8n_deploy_template\n- n8n_workflow_versions\n- n8n_manage_folders (folder CRUD + workflow placement)\n- n8n_manage_credentials (credential CRUD + schema discovery)\n- n8n_audit_instance (security auditing)\n\n### Templates, Data Tables & Self-Help\nSee [OPERATIONS_GUIDE.md](OPERATIONS_GUIDE.md) for:\n- search_templates / get_template / n8n_deploy_template examples\n- n8n_manage_datatable (full actions, filter conditions, examples)\n- tools_documentation, ai_agents_guide, n8n_health_check\n\n---\n\n## Template Usage\n\nThe 2,700+ template library has three tools: `search_templates` (modes `query`/`by_nodes`/`by_task`/`by_metadata`), `get_template` (modes `structure`/`full`), and `n8n_deploy_template` (deploys to your instance with `autoFix`/`autoUpgradeVersions`, returns workflow ID + required credentials + fixes applied).\n\nSee [OPERATIONS_GUIDE.md](OPERATIONS_GUIDE.md) for full search/get/deploy examples.\n\n---\n\n## Running Workflows\n\n`n8n_test_workflow` has one required parameter (`workflowId`) and a `method` that picks the path:\n\n| `method` | Backend | What it does |\n|---|---|---|\n| `auto` (default) | Public API | Detects a webhook/form/chat trigger and fires it over HTTP — the workflow must be **active**. No such trigger → it reports that the workflow cannot be triggered and names the methods below. **`auto` never runs anything through n8n's MCP server.** |\n| `trigger` | Public API | Same HTTP path, requested explicitly. |\n| `prepare` | n8n's MCP server | Read-only: lists the nodes that need pinned data. |\n| `pinned` | n8n's MCP server | Runs the workflow with `pinData` standing in for trigger, credentialed and HTTP Request nodes, and waits. Every other node still runs. A run that finishes in `error`/`crashed`/`canceled` comes back as `EXECUTION_FAILED` with the `executionId`. |\n| `direct` | n8n's MCP server | Starts a run and returns once it has started; nothing is pinned, so every node runs. `message` or `data`/`headers` are forwarded to the trigger as input. |\n\n- The last three need `N8N_MCP_ACCESS_TOKEN` (n8n 2.34+) and the workflow's \"Available in MCP\" setting.\n- `pinData` is keyed by node **name**, and every value is an array of items wrapped as `{\"json\": {...}}` — `{\"Webhook\": [{\"json\": {\"id\": \"123\"}}]}`, never a flat object. It must be non-empty.\n- `triggerNodeName` picks the trigger node to start from (defaults to the detected one; n8n requires it whenever inputs are given).\n- **Both run methods execute the workflow's nodes for real.** `direct` runs every node; `pinned` pins only trigger nodes, nodes with credentials and HTTP Request nodes, so Code, Set, If and credential-free I/O (Execute Command, file read/write) still run. Confirm with the user before running a workflow that writes anywhere.\n- `executionMode` applies to `direct`: `manual` (default) or `production`. It changes the execution context, not whether the run has side effects — a production run goes through the production execution path and is recorded as one. Only pass it when the user asked for one.\n- `timeoutMs` is the client deadline for the official call (5000-600000; default 30000 for `prepare`, 300000 for `pinned`/`direct`).\n- `direct` returns as soon as the run starts, so it reports success with an `executionId` regardless of how the run ends — poll `n8n_executions({action: \"get\", id: executionId})` for the outcome. A dispatch n8n refuses outright comes back as `OFFICIAL_MCP_ERROR`, not `EXECUTION_FAILED`.\n- A workflow whose \"Available in MCP\" setting is off answers `WORKFLOW_NOT_EXPOSED`; `exposeToMcp: true` turns the setting on and retries once. That is a visible, persistent change — ask the user first, and note that enabling it is itself a workflow update, so it can overwrite a concurrent UI edit.\n\nSuccessful and routed responses state `method` and `backend` (`public-api` or `official-mcp`); an envelope rejected on argument validation may carry neither.\n\nSee [WORKFLOW_GUIDE.md](WORKFLOW_GUIDE.md#n8n_test_workflow-running-workflows) for runnable examples of each method.\n\n---\n\n## Version History\n\n`n8n_workflow_versions` reads two independent histories, selected with `source`:\n\n- `source: \"local\"` (default) — the snapshots n8n-mcp takes before it changes a workflow. Any n8n version, no token, ids are numbers. Blind to edits made in the n8n UI. The only source that supports `delete` and `prune`.\n- `source: \"native\"` — n8n's own workflow history, the same list the UI shows, including edits made by people. Needs `N8N_MCP_ACCESS_TOKEN` (n8n 2.34+; the native `diff` needs 2.36, where `get_workflow_versions_diff` shipped) and the workflow's \"Available in MCP\" setting; ids are opaque strings; `list` is capped at 50 with an `offset`; `delete` and `prune` are refused with `MODE_NOT_SUPPORTED_FOR_SOURCE` (n8n owns that retention). Native rollback is not pre-validated — `validateBefore` is accepted and ignored.\n\n`mode: \"diff\"` compares two versions (`versionId` + `toVersionId`, both from the same source and workflow). A local diff (`data.format: \"n8n-mcp\"`) reports added/removed/modified nodes as node **IDs**; a native diff (`data.format: \"n8n\"`) is n8n's own payload with field-level before/after values. Branch on `data.format` rather than assuming field names.\n\nNative modes hit the same consent gate as the routed run methods: a workflow whose \"Available in MCP\" setting is off answers `WORKFLOW_NOT_EXPOSED`, and re-running with `exposeToMcp: true` turns that setting on and retries once (the response then carries `exposedToMcp: true`). It is a visible, persistent change to the workflow — ask the user before passing it. `timeoutMs` (5000-600000) is the client deadline for the native call.\n\nSee [WORKFLOW_GUIDE.md](WORKFLOW_GUIDE.md#n8n_workflow_versions-version-control) for every mode with runnable examples of both sources.\n\n---\n\n## Data Table Management\n\n`n8n_manage_datatable` is the MCP tool for managing data tables and rows from *outside* a workflow (table actions `createTable`/`listTables`/`getTable`/`updateTable`/`deleteTable`; row actions `getRows`/`insertRows`/`updateRows`/`upsertRows`/`deleteRows`, with filtering, pagination, and `dryRun`). Don't confuse it with the in-workflow `nodes-base.dataTable` node, which reads/writes rows *during execution* (see [n8n-node-configuration → OPERATION_PATTERNS.md](../n8n-node-configuration/OPERATION_PATTERNS.md#data-table-nodes-basedatatable)). Rule of thumb: MCP tool to set up a table once, workflow node to read/write on every execution. `deleteRows` requires a filter; use `dryRun: true` before bulk changes.\n\n**Column actions** — `addColumn`, `deleteColumn`, `renameColumn` — change an existing table's columns, which the Public API cannot do; they run through n8n's MCP server and need `N8N_MCP_ACCESS_TOKEN` (n8n 2.34+). `addColumn` takes `column: {name, type}` (name starts with a letter, letters/digits/underscores only, at most 63 chars; type is `string`, `number`, `boolean` or `date`); `deleteColumn`/`renameColumn` take the `columnId` from `getTable`, and `renameColumn` puts the new column name in `name`. They address the table by project: `projectId` is resolved automatically when exactly one project is accessible, otherwise the call returns `PROJECT_REQUIRED` and lists the candidates — pass `projectId` (from `n8n_list_catalog({kind: \"projects\"})`) to skip resolution. Renaming the *table* is not a column action: use `updateTable` on the Public API.\n\n**`deleteColumn` drops the column's values along with the column, and there is no undo.** That bites hardest where you'd least expect it: a column's type cannot be changed after creation, so \"make this column a number\" really means drop-and-re-add, which throws away everything in it. Read the values out with `getRows` first if they matter, and confirm with the user before dropping a populated column.\n\nSee [OPERATIONS_GUIDE.md](OPERATIONS_GUIDE.md) for all actions, filter conditions, and examples.\n\n---\n\n## Workflow Folders\n\n`n8n_manage_folders` organizes workflows into folders (actions `create`/`list`/`get`/`rename`/`move`/`delete`; n8n 2.19+, registered free Community tier and up). `projectId` defaults to `'personal'`. Placing workflows happens in the *workflow* tools: `parentFolderId` on `n8n_create_workflow`, or the `moveToFolder` operation of `n8n_update_partial_workflow` (both n8n 2.32+; `null` = project root). Two things to internalize: a workflow's folder is **write-only** in n8n's API (verify placement via a folder's `get` counts, never by reading the workflow), and `delete` without `transferToFolderId` **archives** the folder's workflows (`transferToFolderId: \"0\"` moves them to the project root instead, keeping them active).\n\nSee [WORKFLOW_GUIDE.md](WORKFLOW_GUIDE.md) for all actions, list filters/counts, and the delete semantics.\n\n---\n\n## Credential Management\n\n`n8n_manage_credentials` is the unified credential tool: actions `list`, `get`, `create`, `update`, `delete`, `getSchema`. It never returns secrets — `get`/`create`/`update` strip the `data` field. Use `getSchema` before `create` to discover required fields. The optional `includeUsage: true` flag (on `list`/`get`) reverse-scans workflows and attaches `usedIn: [{id, name, active}]` + `usageCount` — use it before deleting or rotating a credential to see what breaks (it triggers a full client-side scan, caps at 5000 workflows, excludes archived, and degrades to a `usageScanError` field on failure).\n\nSee [WORKFLOW_GUIDE.md](WORKFLOW_GUIDE.md) for all actions, the includeUsage shape, security notes, and the safe delete/rotate workflow.\n\n---\n\n## Agents\n\nThe three tools in this section exist only for n8n's instance-level MCP server (a separate endpoint from the Public API). `n8n_manage_agents` and `n8n_explore_node_resources` need `N8N_MCP_ACCESS_TOKEN`; `n8n_list_catalog` works without it and uses the token only for its team-project fallback. Other tools route individual operations through the same server — `n8n_test_workflow` `prepare`/`pinned`/`direct`, `n8n_workflow_versions` `source: \"native\"`, the `n8n_manage_datatable` column actions — as described in their own sections; see \"Tool Availability\" below.\n\n- `n8n_manage_agents` — create, configure, validate, run and publish persisted n8n Agents (a standalone assistant artifact: model, instructions, tools, skills, tasks, memory, channels — not the AI Agent workflow node). Actions: `reference`, `search`, `get`, `create`, `mutate`, `validate`, `call`, `publish`, `unpublish`, `revert`, `versions`, `delete`, `discover_assets`, `verify_mcp_server`, `update_integration`. Start with `action: \"reference\"`, then `discover_assets` → `create` → `mutate` (one resource at a time, always the latest hash — n8n returns it as `configHash` and expects it back as `args.baseConfigHash`; a stale one comes back as `STALE_CONFIG`) → `validate`. `publish` only on explicit request; `call` runs the agent with real credentials and tools and may return `approvals[]` for the human to decide. `timeoutMs` is a top-level parameter (default 30000, 180000 for `call`), not part of `args`. Needs n8n **2.34+** with the agents module; on 2.36.x the agents runtime rejects `azureOpenAiApi`/`aws` credentials. Envelope error codes: `NOT_CONFIGURED`, `INVALID_ARGS`, `STALE_CONFIG`, `AGENT_NOT_RUNNABLE`, `AGENT_TOOL_ERROR` (a custom tool that failed to compile, or an unknown `agentId`), plus the shared `OFFICIAL_MCP_*` family (`AUTH_FAILED`, `NOT_ENABLED`, `RATE_LIMITED`, `TOOL_UNAVAILABLE`, `URL_REJECTED`, `TIMEOUT`, `TRANSPORT_ERROR`, `ERROR`). See **n8n-agents** skill's \"Persisted n8n Agents\" section for the full workflow.\n- `n8n_explore_node_resources` — resolve the real values behind a node's `loadOptions` dropdown or resource-locator `listSearch` (Slack channels, Google Sheets tabs, model lists) using a live credential, instead of guessing an ID. Use it when `get_node` (`standard` detail) shows `dynamicOptions: {methodName, methodType, dependsOn}` on a property. **Six parameters are required and none of them are inferred:** `nodeType` (LONG form), `version` (the node `typeVersion` the method belongs to), `methodName` and `methodType` copied verbatim from `dynamicOptions`, and `credentialType` plus a `credentialId` of that type from `n8n_manage_credentials({action: \"list\"})`. Whatever the method `dependsOn` goes in `currentNodeParameters`, resource-locator values keeping their `{__rl: true, mode: \"id\", value: \"…\"}` shape. Each result's `value` is what belongs in the workflow parameter; `name` is display text only.\n- `n8n_list_catalog` — list instance-level `projects` (personal project marked, gives `projectId` for `n8n_manage_agents`/`n8n_manage_datatable`) or `tags`. Works without the token via the Public API; with it configured, falls back to the official MCP server for team projects when the Public API's licence gate refuses (`teamProjectsEnabled` reports which).\n\n---\n\n## Security & Audit\n\n`n8n_audit_instance` combines n8n's built-in audit (categories `credentials`/`database`/`nodes`/`instance`/`filesystem`) with a custom deep scan (`hardcoded_secrets`, `unauthenticated_webhooks`, `error_handling`, `data_retention`). All parameters optional: `categories`, `includeCustomScan` (default `true`), `customChecks`, `daysAbandonedWorkflow`. Detected secrets are masked (first 6 + last 4 chars). Output is an actionable markdown report — summary table, findings by workflow, and a Remediation Playbook split into auto-fixable / requires-review / requires-user-action.\n\nSee [WORKFLOW_GUIDE.md](WORKFLOW_GUIDE.md) for the two scanning approaches, examples, and remediation types in full.\n\n---\n\n## Self-Help Tools\n\n- `tools_documentation()` — overview of all tools; `tools_documentation({topic, depth: \"full\"})` for a specific tool. Code node guides via topics `javascript_code_node_guide` / `python_code_node_guide`.\n- **AI agent guide** — `tools_documentation({topic: \"ai_agents_guide\", depth: \"full\"})` (no standalone tool); returns architecture, connections, tools, validation, best practices.\n- `n8n_health_check()` — quick check; `n8n_health_check({mode: \"diagnostic\"})` returns status, env vars, tool status, API connectivity. Both modes also return an **`officialMcp`** block — `{configured, endpoint, reachable, toolCount, agentTools}` — the preflight for everything gated on `N8N_MCP_ACCESS_TOKEN`: the agent tools, `n8n_test_workflow`'s routed methods, native version history, the data-table column actions. Read it once before reaching for any of them, rather than discovering the gap through a `NOT_CONFIGURED` envelope mid-task.\n\nSee [OPERATIONS_GUIDE.md](OPERATIONS_GUIDE.md) for examples.\n\n---\n\n## Tool Availability\n\n**Always Available** (no n8n API needed):\n- search_nodes, get_node\n- validate_node, validate_workflow\n- search_templates, get_template\n- tools_documentation (includes the ai_agents_guide topic)\n\n**Requires n8n API** (N8N_API_URL + N8N_API_KEY):\n- n8n_create_workflow\n- n8n_update_partial_workflow, n8n_update_full_workflow\n- n8n_validate_workflow (by ID)\n- n8n_list_workflows, n8n_get_workflow, n8n_delete_workflow\n- n8n_test_workflow\n- n8n_executions\n- n8n_evaluations (reads: n8n 2.30+ with an API key created on 2.30+; run/cancel: n8n 2.32+ with a key created on 2.32+ — older keys lack the testRun scopes)\n- n8n_deploy_template\n- n8n_workflow_versions\n- n8n_autofix_workflow\n- n8n_manage_datatable\n- n8n_manage_folders (folder CRUD: n8n 2.19+, registered Community tier and up; workflow placement via parentFolderId/moveToFolder: n8n 2.32+)\n- n8n_manage_credentials\n- n8n_audit_instance\n- n8n_list_catalog (works without the token; needs it only for the team-project fallback)\n\n**Requires `N8N_MCP_ACCESS_TOKEN`** (a separate token from n8n Settings → Instance-level MCP, in addition to the Public API credentials above):\n- n8n_manage_agents\n- n8n_explore_node_resources\n- n8n_test_workflow with `method: \"prepare\"`/`\"pinned\"`/`\"direct\"` (also needs the workflow's \"Available in MCP\" setting)\n- n8n_workflow_versions with `source: \"native\"` (also needs the workflow's \"Available in MCP\" setting)\n- n8n_manage_datatable with `addColumn`/`deleteColumn`/`renameColumn`\n\nIf API tools unavailable, use templates and validation-only workflows.\n\n---\n\n## Unified Tool Reference\n\n- **`get_node`** — detail levels (`minimal` ~200 tok / `standard` ~1-2K, RECOMMENDED / `full` ~3-8K, sparingly) and modes (`info` default, `docs`, `search_properties` + `propertyQuery`, `versions`, `compare`, `breaking`, `migrations`). Deep dive in [SEARCH_GUIDE.md](SEARCH_GUIDE.md).\n- **`validate_node`** — modes `full` (default, errors/warnings/suggestions) and `minimal` (required-fields check); profiles `minimal`/`runtime` (default, recommended)/`ai-friendly`/`strict`. Deep dive in [VALIDATION_GUIDE.md](VALIDATION_GUIDE.md).\n\n---\n\n## Performance Characteristics\n\n| Tool | Response Time | Payload Size |\n|------|---------------|--------------|\n| search_nodes | <20ms | Small |\n| get_node (standard) | <10ms | ~1-2KB |\n| get_node (full) | <100ms | 3-8KB |\n| validate_node (minimal) | <50ms | Small |\n| validate_node (full) | <100ms | Medium |\n| validate_workflow | 100-500ms | Medium |\n| n8n_manage_folders | 100-500ms | Small |\n| n8n_manage_credentials | 50-500ms | Small-Medium |\n| n8n_audit_instance | 500-5000ms | Large |\n| n8n_create_workflow | 100-500ms | Medium |\n| n8n_update_partial_workflow | 50-200ms | Small |\n| n8n_deploy_template | 200-500ms | Medium |\n\n---\n\n## Best Practices\n\n### Do\n- For simple workflows (<=5 nodes), use MCP tools directly — don't over-engineer the investigation\n- Use `patchNodeField` for surgical edits to Code node content instead of replacing the entire node\n- Use `get_node({detail: \"standard\"})` for most use cases\n- Specify validation profile explicitly (`profile: \"runtime\"`)\n- Use smart parameters (`branch`, `case`) for clarity\n- Include `intent` parameter in workflow updates\n- Follow search → get_node → validate workflow\n- Iterate workflows (avg 56s between edits)\n- Validate after every significant change\n- Use `includeExamples: true` for real configs\n- Use `n8n_deploy_template` for quick starts\n\n### Don't\n- Use `detail: \"full\"` unless necessary (wastes tokens)\n- Forget nodeType prefix (`nodes-base.*`)\n- Skip validation profiles\n- Try to build workflows in one shot (iterate!)\n- Ignore auto-sanitization behavior\n- Use full prefix (`n8n-nodes-base.*`) with search/validate tools\n- Forget to activate workflows after building\n\n---\n\n## Summary\n\n**Most Important**:\n1. Use **get_node** with `detail: \"standard\"` (default) - covers 95% of use cases\n2. nodeType formats differ: `nodes-base.*` (search/validate) vs `n8n-nodes-base.*` (workflows)\n3. Specify **validation profiles** (`runtime` recommended)\n4. Use **smart parameters** (`branch=\"true\"`, `case=0`)\n5. Include **intent parameter** in workflow updates\n6. **Auto-sanitization** runs on ALL nodes during updates\n7. Workflows can be **activated via API** (`activateWorkflow` operation)\n8. Workflows are built **iteratively** (56s avg between edits)\n9. **Data tables** managed with `n8n_manage_datatable` (CRUD + filtering)\n10. **Folders** managed with `n8n_manage_folders`; workflow placement is write-only (verify via folder counts, not the workflow)\n11. **Credentials** managed with `n8n_manage_credentials` (CRUD + schema discovery)\n12. **Security audits** via `n8n_audit_instance` (built-in + custom deep scan)\n13. **AI agent guide** available via `tools_documentation({topic: \"ai_agents_guide\", depth: \"full\"})`\n\n**Common Workflow**:\n1. search_nodes → find node\n2. get_node → understand config\n3. validate_node → check config\n4. n8n_create_workflow → build\n5. n8n_validate_workflow → verify\n6. n8n_update_partial_workflow → iterate\n7. activateWorkflow → go live!\n\nFor details, see:\n- [SEARCH_GUIDE.md](SEARCH_GUIDE.md) - Node discovery\n- [VALIDATION_GUIDE.md](VALIDATION_GUIDE.md) - Configuration validation + common mistakes\n- [WORKFLOW_GUIDE.md](WORKFLOW_GUIDE.md) - Workflow management\n- [OPERATIONS_GUIDE.md](OPERATIONS_GUIDE.md) - Templates, data tables, self-help tools\n\n---\n\n**Related Skills**:\n- n8n Expression Syntax - Write expressions in workflow fields\n- n8n Workflow Patterns - Architectural patterns from templates\n- n8n Validation Expert - Interpret validation errors\n- n8n Node Configuration - Operation-specific requirements\n- n8n Code JavaScript - Write JavaScript in Code nodes\n- n8n Code Python - Write Python in Code nodes\n","frontmatter":{"name":"n8n-mcp-tools-expert","description":"Expert guide for using n8n-mcp MCP tools effectively. Use when searching for nodes, validating configurations, accessing templates, managing workflows, organizing workflows into folders, managing credentials, auditing instance security, or using any n8n-mcp tool. Provides tool selection guidance, parameter formats, and common patterns. IMPORTANT — Always consult this skill before calling any n8n-mcp tool — it prevents common mistakes like wrong nodeType formats, incorrect parameter structures, and inefficient tool usage. If the user mentions n8n, workflows, nodes, or automation and you have n8n MCP tools available, use this skill first."},"isInternal":false,"tokens":7839,"sizeBytes":30447},{"name":"VALIDATION_GUIDE.md","path":"data/skills/n8n-mcp-tools-expert/VALIDATION_GUIDE.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-mcp-tools-expert/VALIDATION_GUIDE.md","title":"N8n-mcp-tools-expert Skill","category":"anthropic-skill","format":"markdown","content":"# N8n-mcp-tools-expert Skill\n\nPath: `data/skills/n8n-mcp-tools-expert/VALIDATION_GUIDE.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-mcp-tools-expert/VALIDATION_GUIDE.md)","tokens":300,"sizeBytes":233},{"name":"WORKFLOW_GUIDE.md","path":"data/skills/n8n-mcp-tools-expert/WORKFLOW_GUIDE.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-mcp-tools-expert/WORKFLOW_GUIDE.md","title":"N8n-mcp-tools-expert Skill","category":"anthropic-skill","format":"markdown","content":"# N8n-mcp-tools-expert Skill\n\nPath: `data/skills/n8n-mcp-tools-expert/WORKFLOW_GUIDE.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-mcp-tools-expert/WORKFLOW_GUIDE.md)","tokens":300,"sizeBytes":229},{"name":"README.md","path":"data/skills/n8n-multi-instance/README.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-multi-instance/README.md","title":"N8n-multi-instance Skill","category":"anthropic-skill","format":"markdown","content":"# N8n-multi-instance Skill\n\nPath: `data/skills/n8n-multi-instance/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-multi-instance/README.md)","tokens":300,"sizeBytes":207},{"name":"SKILL.md","path":"data/skills/n8n-multi-instance/SKILL.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-multi-instance/SKILL.md","title":"n8n-multi-instance","category":"anthropic-skill","format":"markdown","content":"---\nname: n8n-multi-instance\ndescription: Use when an n8n-mcp account targets more than one n8n instance — i.e. the `n8n_instances` tool is available, the user mentions multiple n8n instances or environments (prod vs staging, several teams or clients), a workflow / datatable / credential / execution call returns an unexpected NOT_FOUND or reads data you don't recognize, or a credential create/update/delete is refused with an `INSTANCE_AMBIGUOUS` error. Covers choosing and switching which instance this MCP session targets, verifying the target before high-stakes work — credential writes above all — and recovering from misroutes and ambiguous-write fail-closes. Always consult this skill before operating on a specific instance, before any credential create/update/delete on a multi-instance account, or when a call hits the wrong/empty data or an `INSTANCE_AMBIGUOUS` error.\n---\n\n# Working with multiple n8n instances over MCP\n\nWhen the `n8n_instances` tool is available, the user has **multi-instance mode** on: one MCP\nconnection can reach several n8n instances (e.g. `prod`, `staging`, or one per client/team).\nEvery other n8n tool (`n8n_get_workflow`, `n8n_list_workflows`, `n8n_update_partial_workflow`,\n`n8n_manage_datatable`, `n8n_manage_credentials`, `n8n_executions`, `n8n_test_workflow`, …) runs\nagainst **whichever instance this session is currently targeting**. There is no per-call instance\nargument: you change the target only by switching. Target the wrong instance and a read returns the\nwrong data and a write lands in the wrong place — usually with **no error** (the one exception is an\nambiguous credential write, which fails closed; see below). So target deliberately.\n\nIf the `n8n_instances` tool is **not** present, the account is single-instance: ignore this skill\nand use the n8n tools directly.\n\n## Golden rules\n\nSix rules. Each prevents a class of silent misroute.\n\n1. **Discover first.** Call `n8n_instances({mode:\"list\"})` before acting so you know the instance\n   names and which one is `current`.\n2. **Switch by name to your target** before doing work on a non-default instance:\n   `n8n_instances({mode:\"switch\", name:\"<instance name>\"})`. The match is case-insensitive.\n3. **Switch in its own turn.** Never put a `switch` and a dependent operation in the **same\n   parallel tool-call batch**. Calls in one batch have no guaranteed order, so the dependent call\n   can be resolved against the *previous* instance before the switch's session state is visible.\n   Switch, let it return, *then* operate.\n4. **Verify before high-stakes ops.** Immediately before creating/updating/deleting **credentials**\n   (and before destructive workflow edits), confirm `current` is the instance you intend — primary\n   check is `n8n_instances({mode:\"list\"})`. The system fail-closes only the *ambiguous* credential\n   case (rule 6); an explicit switch to the **wrong** instance still writes there silently, so this\n   check is on you.\n5. **An unexpected `NOT_FOUND` is almost always a wrong-instance misroute, not a deletion.** Don't\n   recreate the object. Re-check the current instance and retry (see Recovery).\n6. **On `INSTANCE_AMBIGUOUS`, switch on *this* session, then retry.** The system is refusing to\n   write a secret because this session never picked a target itself. Comply — run `switch` here to\n   confirm the instance, then retry the write. Don't work around it or retry blindly.\n\n## Core workflow\n\n```\n1. n8n_instances({mode:\"list\"})                      # see available[] + current + default\n2. n8n_instances({mode:\"switch\", name:\"prod\"})       # bind THIS session to \"prod\"\n   → returns { previous, current }; confirm current.name == \"prod\"\n3. (do your work) n8n_list_workflows / n8n_get_workflow / n8n_manage_datatable / ...\n4. Before a credential write or a delete:\n   n8n_instances({mode:\"list\"})  → re-confirm current, THEN n8n_manage_credentials({action:\"create\", ...})\n```\n\nTo move to another instance, just `switch` again. The whole session follows the switch.\n\n## The `n8n_instances` tool\n\nTwo modes (`mode` is required and enum-validated):\n\n- `{mode:\"list\"}` → `{ current, default, available }`, no side effects.\n  - `current` and `default` are each one instance `{ id, name, url, isDefault }` (or `null`).\n  - `available` is every instance, each with an extra `isCurrent` boolean. Match by **`name`**;\n    never hard-code `id`.\n- `{mode:\"switch\", name:\"<name>\"}` → `{ previous, current }`, and binds this session to the named\n  instance. `name` is case-insensitive.\n\n### Error envelope (from the `n8n_instances` tool)\n\nEvery error returns `{ error: \"<CODE>\", message, … }`. The ones you'll actually hit:\n\n| Code | When | What to do |\n|---|---|---|\n| `UNKNOWN_INSTANCE` | `name` matches no instance | Pick a name from the `available` list in the error payload and retry. |\n| `NAME_REQUIRED` | `switch` with no `name` | Re-call with a `name` (the error lists the valid ones in `available`). |\n| `MULTI_INSTANCE_DISABLED` | multi-instance mode is off | There's nothing to switch; use the n8n tools directly. The user can enable it at the n8n-mcp dashboard. |\n| `NO_SESSION` | the request has **neither** an MCP session id **nor** a credential id | A selection has nowhere to land. Reconnect / initialize a session, then switch. |\n| `UNKNOWN_MODE` | `mode` wasn't `list`/`switch` | Use `list` or `switch`. |\n| `INVALID_CONTEXT` | server-side metadata missing | A server bug, not your input — report it. |\n\n> Instance names can never be `default`, `current`, `list`, or `switch` (reserved), so you'll never\n> see an instance literally named after a mode or field.\n\n### `INSTANCE_AMBIGUOUS` (from the credential-write path, not the tool)\n\nA separate, higher-stakes error. It is **not** returned by `n8n_instances` — it's returned by the\nserver when you call `n8n_manage_credentials` to **create/update/delete** a credential and the target\ninstance is ambiguous: this session never switched on its own but inherited a switch made elsewhere\n(a fan-out / reconnect), pointing at a **non-default** instance. Rather than risk writing a secret to\nthe wrong instance, the server **blocks the write** (it never reaches n8n, no quota is charged) and\nreturns:\n\n```json\n{\n  \"error\": \"INSTANCE_AMBIGUOUS\",\n  \"message\": \"… the session issuing this request never switched there itself … Re-run n8n_instances({mode:\\\"switch\\\", name:\\\"…\\\"}) on this session to confirm the target …\",\n  \"lastSelected\": { \"id\": \"…\", \"name\": \"…\" },\n  \"default\":      { \"id\": \"…\", \"name\": \"…\" }\n}\n```\n\n**Fix:** decide which instance you actually want (`lastSelected` is the inherited switch, `default`\nis the account default), run `n8n_instances({mode:\"switch\", name:\"…\"})` on **this** session, then\nretry the write. See rule 6.\n\n## How targeting behaves (mental model)\n\n- A `switch` **binds this session** to the chosen instance. The binding **persists for the rest of\n  the session and survives reconnects, idle, and backend deploys** (~24h, the MCP session lifetime)\n  — you should not need to re-switch before every call.\n- Other sessions / terminals are **independent**: switching here does not move them.\n- One session targets **one instance at a time**. There is no per-call instance argument; you\n  change the target only via `switch`.\n- **Reads and non-credential writes** route to the currently-selected instance, silently — a\n  misroute produces wrong data or a `NOT_FOUND`, not an error.\n- **Credential writes are the one guarded case.** They route the same way, except the server\n  fail-closes the *ambiguous* state (a session that never switched, recovered onto a non-default\n  instance) with `INSTANCE_AMBIGUOUS`. This is a safety net, not a substitute for rule 4: an\n  explicit switch to the wrong instance still writes there.\n- **If your selected instance is deleted** (the user removes it mid-session), the next call silently\n  falls back to your **default** instance — no error. So default's data appearing where you expected\n  another instance's can look like \"my data vanished.\" Re-list to see where you are.\n\n## Recovery playbook\n\n| Symptom | What it usually means | Do this |\n|---|---|---|\n| `INSTANCE_AMBIGUOUS` on a credential create/update/delete | This session never switched itself; the system won't guess which instance to write the secret to | Run `n8n_instances({mode:\"switch\", name:\"<target>\"})` on this session (the error names `lastSelected` and `default` — pick the one you want), then retry the write. Never retry blindly. |\n| `NOT_FOUND` for a workflow/datatable/credential you **know exists** | You're pointed at the wrong instance — **not** that it was deleted | `n8n_instances({mode:\"list\"})` → check `current`. If it's not your target, `switch` and retry. **Do not recreate the object.** |\n| A read returns **empty or unfamiliar** data | Wrong-instance read, or a silent fallback to `default` after your instance was deleted | `n8n_instances({mode:\"list\"})`, confirm `current`, switch if needed, re-read before drawing conclusions. |\n| `UNKNOWN_INSTANCE` on `switch` | The `name` is wrong (typo, or you guessed) | Read the `available` names in the error and switch to one of those. Names are case-insensitive. |\n| `n8n_health_check` reports an `instanceName` you didn't expect | This session is on a different instance than you think | `switch` to the intended instance, then proceed. |\n| Repeated misroutes within one turn | You batched a `switch` with dependent work | Split them: `switch` alone, await the result, then operate one logical step at a time. |\n\nAfter any recovery switch, sanity-check with `n8n_instances({mode:\"list\"})` (read `current`) as the\nprimary signal. `n8n_health_check` also returns the resolved instance under `details.instanceName`,\nbut it can be absent on some paths (legacy/chat), so treat it as a secondary confirmation.\n\n## Credential operations (highest stakes)\n\nCredentials hold live secrets, and a misrouted credential write puts a secret on the **wrong\ninstance**. The server protects the **ambiguous** case automatically — if this session never picked\na target and inherited a switch to a non-default instance, the write fails closed with\n`INSTANCE_AMBIGUOUS` (rule 6) and never reaches n8n. But that net is narrow: a credential write on a\nsession that **did** switch goes through to whatever instance it switched to, with no second\nguess. So:\n\n- **Verify `current` immediately before** `n8n_manage_credentials` create/update/delete — call\n  `n8n_instances({mode:\"list\"})` in the same short sequence, not 10 steps earlier where a later\n  switch could have moved you.\n- **On `INSTANCE_AMBIGUOUS`**, switch on this session to confirm the target, then retry — don't\n  work around it.\n- Credential **reads** (`action:\"list\"`/`\"get\"`/`\"getSchema\"`) are not gated and don't write a\n  secret, but a read off the wrong instance returns the wrong schema or list — so still verify\n  `current` if the result looks wrong.\n- For the `n8n_manage_credentials` tool itself (CRUD shapes, `getSchema` discovery, never inlining\n  secrets into text fields), see `n8n-mcp-tools-expert`.\n\n## Common multi-instance task: copy something between instances\n\nTo recreate a credential or workflow from instance A on instance B:\n\n```\n1. switch → A;  read the source (n8n_manage_credentials get / n8n_get_workflow)\n2. switch → B   (its own call — never batched with the create below)\n3. n8n_instances({mode:\"list\"})  → confirm current == B\n4. create on B  (n8n_manage_credentials create / n8n_create_workflow)\n```\n\nDo each instance's steps in its own turn; never overlap `switch → B` with the create-on-B call\n(rule 3), and switch explicitly on this session before the credential write so it isn't ambiguous\n(rules 4 and 6).\n\n## Quick reference\n\n- See instances + where you are: `n8n_instances({mode:\"list\"})` → `{ current, default, available }`\n- Change target: `n8n_instances({mode:\"switch\", name:\"<name>\"})` — its own turn, then operate\n- Confirm target: `current` from `list` (primary); `details.instanceName` from `n8n_health_check` (secondary, may be absent)\n- `UNKNOWN_INSTANCE` → switch to a name from the error's `available` list, then retry\n- `INSTANCE_AMBIGUOUS` (credential write) → `switch` on this session to confirm the target, then retry\n- Unexpected `NOT_FOUND` → verify the instance, switch, retry; **do not recreate**\n- Before credential writes → re-`list`, confirm `current`, then write (the fail-close only covers the ambiguous case)\n\n## Integration with other skills\n\n- **n8n-mcp-tools-expert** — owns `n8n_manage_credentials` (CRUD + `getSchema`) and the rule that\n  secrets go through the credential system, never text fields. This skill adds the \"which instance?\"\n  layer on top.\n- **using-n8n-mcp-skills** — the router; consult it for which skill owns a given build step.\n","frontmatter":{"name":"n8n-multi-instance","description":"Use when an n8n-mcp account targets more than one n8n instance — i.e. the `n8n_instances` tool is available, the user mentions multiple n8n instances or environments (prod vs staging, several teams or clients), a workflow / datatable / credential / execution call returns an unexpected NOT_FOUND or reads data you don't recognize, or a credential create/update/delete is refused with an `INSTANCE_AMBIGUOUS` error. Covers choosing and switching which instance this MCP session targets, verifying the target before high-stakes work — credential writes above all — and recovering from misroutes and ambiguous-write fail-closes. Always consult this skill before operating on a specific instance, before any credential create/update/delete on a multi-instance account, or when a call hits the wrong/empty data or an `INSTANCE_AMBIGUOUS` error."},"isInternal":false,"tokens":3171,"sizeBytes":12843},{"name":"DEPENDENCIES.md","path":"data/skills/n8n-node-configuration/DEPENDENCIES.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-node-configuration/DEPENDENCIES.md","title":"N8n-node-configuration Skill","category":"anthropic-skill","format":"markdown","content":"# N8n-node-configuration Skill\n\nPath: `data/skills/n8n-node-configuration/DEPENDENCIES.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-node-configuration/DEPENDENCIES.md)","tokens":300,"sizeBytes":231},{"name":"NODE_FAMILY_GOTCHAS.md","path":"data/skills/n8n-node-configuration/NODE_FAMILY_GOTCHAS.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-node-configuration/NODE_FAMILY_GOTCHAS.md","title":"N8n-node-configuration Skill","category":"anthropic-skill","format":"markdown","content":"# N8n-node-configuration Skill\n\nPath: `data/skills/n8n-node-configuration/NODE_FAMILY_GOTCHAS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-node-configuration/NODE_FAMILY_GOTCHAS.md)","tokens":300,"sizeBytes":245},{"name":"OPERATION_PATTERNS.md","path":"data/skills/n8n-node-configuration/OPERATION_PATTERNS.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-node-configuration/OPERATION_PATTERNS.md","title":"N8n-node-configuration Skill","category":"anthropic-skill","format":"markdown","content":"# N8n-node-configuration Skill\n\nPath: `data/skills/n8n-node-configuration/OPERATION_PATTERNS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-node-configuration/OPERATION_PATTERNS.md)","tokens":300,"sizeBytes":243},{"name":"README.md","path":"data/skills/n8n-node-configuration/README.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-node-configuration/README.md","title":"N8n-node-configuration Skill","category":"anthropic-skill","format":"markdown","content":"# N8n-node-configuration Skill\n\nPath: `data/skills/n8n-node-configuration/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-node-configuration/README.md)","tokens":300,"sizeBytes":219},{"name":"SKILL.md","path":"data/skills/n8n-node-configuration/SKILL.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-node-configuration/SKILL.md","title":"n8n-node-configuration","category":"anthropic-skill","format":"markdown","content":"---\nname: n8n-node-configuration\ndescription: Operation-aware node configuration guidance. Use when configuring nodes, understanding property dependencies, determining required fields, choosing between get_node detail levels, or learning common configuration patterns by node type. Always use this skill when setting up node parameters — it explains which fields are required for each operation, how displayOptions control field visibility, and when to use patchNodeField for surgical edits vs full node updates.\n---\n\n# n8n Node Configuration\n\nExpert guidance for operation-aware node configuration with property dependencies.\n\n---\n\n## Configuration Philosophy\n\n**Progressive disclosure**: Start minimal, add complexity as needed\n\nConfiguration best practices:\n- `get_node` with `detail: \"standard\"` is the most used discovery pattern\n- 56 seconds average between configuration edits\n- Covers 95% of use cases with 1-2K tokens response\n\n**Key insight**: Most configurations need only standard detail, not full schema!\n\n---\n\n## Core Concepts\n\n### 1. Operation-Aware Configuration\n\n**Not all fields are always required** - it depends on operation!\n\n**Example**: Slack node\n```javascript\n// For operation='post'\n{\n  \"resource\": \"message\",\n  \"operation\": \"post\",\n  \"channel\": \"#general\",  // Required for post\n  \"text\": \"Hello!\"        // Required for post\n}\n\n// For operation='update'\n{\n  \"resource\": \"message\",\n  \"operation\": \"update\",\n  \"messageId\": \"123\",     // Required for update (different!)\n  \"text\": \"Updated!\"      // Required for update\n  // channel NOT required for update\n}\n```\n\n**Key**: Resource + operation determine which fields are required!\n\n### 2. Property Dependencies\n\n**Fields appear/disappear based on other field values**\n\n**Example**: HTTP Request node\n```javascript\n// When method='GET'\n{\n  \"method\": \"GET\",\n  \"url\": \"https://api.example.com\"\n  // sendBody not shown (GET doesn't have body)\n}\n\n// When method='POST'\n{\n  \"method\": \"POST\",\n  \"url\": \"https://api.example.com\",\n  \"sendBody\": true,       // Now visible!\n  \"body\": {               // Required when sendBody=true\n    \"contentType\": \"json\",\n    \"content\": {...}\n  }\n}\n```\n\n**Mechanism**: displayOptions control field visibility\n\n### 3. Progressive Discovery\n\n**Use the right detail level**:\n\n1. **get_node({detail: \"standard\"})** - DEFAULT\n   - Quick overview (~1-2K tokens)\n   - Required fields + common options\n   - **Use first** - covers 95% of needs\n\n2. **get_node({mode: \"search_properties\", propertyQuery: \"...\"})** (for finding specific fields)\n   - Find properties by name\n   - Use when looking for auth, body, headers, etc.\n\n3. **get_node({detail: \"full\"})** (complete schema)\n   - All properties (~3-8K tokens)\n   - Use only when standard detail is insufficient\n\n---\n\n## Configuration Workflow\n\n### Standard Process\n\n1. Identify node type and operation.\n2. Use `get_node` (standard detail is default).\n3. Configure required fields.\n4. Validate configuration.\n5. If a field is unclear → `get_node({mode: \"search_properties\"})`.\n6. Add optional fields as needed.\n7. Validate again.\n8. Deploy.\n\n### Example: Configuring HTTP Request\n\nThe validate-driven loop in practice: start minimal (`method`, `url`, `authentication`), then let each `validate_node` error surface the next required field (`sendBody` for POST → `body` when `sendBody=true`) until valid. Full step-by-step walkthrough in **[OPERATION_PATTERNS.md](OPERATION_PATTERNS.md#worked-example-configuring-http-request-step-by-step)**.\n\n---\n\n## get_node Detail Levels\n\n### Standard Detail (DEFAULT - Use This!)\n\n**✅ Starting configuration**\n```javascript\nget_node({\n  nodeType: \"nodes-base.slack\"\n});\n// detail=\"standard\" is the default\n```\n\n**Returns** (~1-2K tokens):\n- Required fields\n- Common options\n- Operation list\n- Metadata\n\n**Use**: 95% of configuration needs\n\n### Full Detail (Use Sparingly)\n\n**✅ When standard isn't enough**\n```javascript\nget_node({\n  nodeType: \"nodes-base.slack\",\n  detail: \"full\"\n});\n```\n\n**Returns** (~3-8K tokens):\n- Complete schema\n- All properties\n- All nested options\n\n**Warning**: Large response, use only when standard insufficient\n\n### Search Properties Mode\n\n**✅ Looking for specific field**\n```javascript\nget_node({\n  nodeType: \"nodes-base.httpRequest\",\n  mode: \"search_properties\",\n  propertyQuery: \"auth\"\n});\n```\n\n**Use**: Find authentication, headers, body fields, etc.\n\n### Decision Tree\n\n1. Starting a new node config → `get_node` (standard).\n2. Standard has what you need → configure with it. Otherwise continue.\n3. Looking for a specific field → `search_properties` mode. Otherwise continue.\n4. Still need more → `get_node({detail: \"full\"})`.\n\n**Dynamic properties**: when `standard` detail marks a property with `dynamicOptions: {methodName, methodType, dependsOn}`, its real values come from a live `loadOptions`/`listSearch` method, not from bundled docs — don't guess an ID for it. Resolve it with `n8n_explore_node_resources` (needs `N8N_MCP_ACCESS_TOKEN`, n8n 2.34+) and put the returned `value` in the config; `name` is display text only.\n\nAll six parameters are required and none are inferred from each other:\n\n```javascript\nn8n_explore_node_resources({\n  nodeType: \"n8n-nodes-base.googleSheets\",  // LONG form\n  version: 4.5,                              // the node typeVersion the method belongs to\n  methodName: \"getSheets\",                   // copied verbatim from dynamicOptions\n  methodType: \"listSearch\",                  // \"listSearch\" for resource locators, \"loadOptions\" for plain dropdowns\n  credentialType: \"googleSheetsOAuth2Api\",\n  credentialId: \"c2\",                        // from n8n_manage_credentials({action: \"list\"})\n  currentNodeParameters: {                   // whatever dependsOn names, in its real shape\n    documentId: {__rl: true, mode: \"id\", value: \"1AbC…\"}\n  }\n})\n```\n\n`dependsOn` names the parameters the method needs already chosen — pass them in `currentNodeParameters`, keeping resource-locator values in their `{__rl: true, mode, value}` shape, or the method returns nothing useful. `methodName` is case-sensitive and specific to the `nodeType` + `version` pair; a mismatch returns `OFFICIAL_MCP_ERROR` rather than an empty list.\n\n---\n\n## Property Dependencies Deep Dive\n\nFields have `displayOptions` visibility rules: `show`/`hide` blocks where multiple conditions are AND'd and multiple values are OR'd (e.g. `body` shows when `sendBody=true` AND `method IN (POST, PUT, PATCH)`). The three recurring patterns are the boolean toggle (sendBody → body), the operation switch (post vs update show different fields), and type selection (string vs boolean conditions). To find what controls a field, use `get_node({mode: \"search_properties\", propertyQuery: \"...\"})` or `get_node({detail: \"full\"})` — especially when validation flags a field you don't see.\n\nMechanism details, all four dependency patterns, complex flows, nested dependencies, and troubleshooting are in **[DEPENDENCIES.md](DEPENDENCIES.md)** (quick-reference recap under [Quick Reference: displayOptions and Common Dependency Patterns](DEPENDENCIES.md#quick-reference-displayoptions-and-common-dependency-patterns)).\n\n---\n\n## Common Node Patterns\n\n### Pattern 1: Resource/Operation Nodes\n\n**Examples**: Slack, Google Sheets, Airtable\n\n**Structure**:\n```javascript\n{\n  \"resource\": \"<entity>\",      // What type of thing\n  \"operation\": \"<action>\",     // What to do with it\n  // ... operation-specific fields\n}\n```\n\n**How to configure**:\n1. Choose resource\n2. Choose operation\n3. Use get_node to see operation-specific requirements\n4. Configure required fields\n\n### Pattern 2: HTTP-Based Nodes\n\n**Examples**: HTTP Request, Webhook\n\n**Structure**:\n```javascript\n{\n  \"method\": \"<HTTP_METHOD>\",\n  \"url\": \"<endpoint>\",\n  \"authentication\": \"<type>\",\n  // ... method-specific fields\n}\n```\n\n**Dependencies**:\n- POST/PUT/PATCH → sendBody available\n- sendBody=true → body required\n- authentication != \"none\" → credentials required\n\n**Critical: credentials block, node id, typeVersion**\n- **Never set a placeholder credential ID** (e.g. `\"id\": \"REPLACE_ME\"`) — n8n's UI renders a permanently disabled credential selector for unknown IDs. Omit the `credentials` block when the real ID is unknown; the user then gets a normal clickable dropdown.\n- **Node `id` must be a UUID v4**, not a readable slug — the frontend binds forms and the credential component to it.\n- **Don't hardcode old `typeVersion` values** — verify the current version with `get_node` (httpRequest is 4.4+).\n\n### Pattern 3: Database Nodes\n\n**Examples**: Postgres, MySQL, MongoDB\n\n**Structure**:\n```javascript\n{\n  \"operation\": \"<query|insert|update|delete>\",\n  // ... operation-specific fields\n}\n```\n\n**Dependencies**:\n- operation=\"executeQuery\" → query required\n- operation=\"insert\" → table + values required\n- operation=\"update\" → table + values + where required\n\n**Critical: Write operations may return 0 items**\n- INSERT, UPDATE, DELETE can produce 0 n8n output items, depending on the node and operation (raw query execution reliably returns 0 result rows; some database nodes return the affected rows)\n- Set `alwaysOutputData: true` on write-operation nodes to keep downstream chains alive\n- Downstream nodes should use `$('UpstreamNode').all()` instead of `$input` if they need data\n\n### Pattern 4: Conditional Logic Nodes\n\n**Examples**: IF, Switch, Merge\n\n**Structure**:\n```javascript\n{\n  \"conditions\": {\n    \"<type>\": [\n      {\n        \"operation\": \"<operator>\",\n        \"value1\": \"...\",\n        \"value2\": \"...\"  // Only for binary operators\n      }\n    ]\n  }\n}\n```\n\n**Dependencies**:\n- Binary operators (equals, contains, etc.) → value1 + value2\n- Unary operators (isEmpty, isNotEmpty) → value1 only + singleValue: true\n\n---\n\n## Operation-Specific Configuration\n\nRequired fields shift with resource + operation: Slack `post` needs `channel`+`text`, but `update` needs `messageId`+`text` (channel optional) and `channel/create` needs `name`. HTTP `GET` uses `sendQuery`+`queryParameters`; `POST` needs `sendBody`+`body`. IF binary operators (`equals`) need `value1`+`value2`; unary (`isEmpty`) need only `value1` plus auto-added `singleValue: true`. Concrete minimal configs for each in **[OPERATION_PATTERNS.md](OPERATION_PATTERNS.md#operation-specific-configuration-examples)**.\n\n---\n\n## Handling Conditional Requirements\n\nSome fields are required only under certain conditions: HTTP `body` is required when `sendBody=true` AND `method IN (POST, PUT, PATCH, DELETE)`; IF `singleValue` should be `true` when the operator is unary (`isEmpty`, `isNotEmpty`, `true`, `false`) — and auto-sanitization sets it for you. Discover conditional requirements by reading the validation error, searching the property (`get_node({mode: \"search_properties\"})`), or iterating from a minimal config. Worked discovery examples in **[DEPENDENCIES.md](DEPENDENCIES.md#handling-conditional-requirements)**.\n\n---\n\n## Node-Specific Configuration Notes\n\n### SplitInBatches v3\n\n```javascript\n{\n  \"batchSize\": 100,  // Number of items per batch\n  \"options\": {}\n}\n```\n\n**Output wiring**:\n- `main[0]` (done) → Connect to downstream processing (add Limit 1 first)\n- `main[1]` (each batch) → Connect to loop body, then loop back to SplitInBatches input\n\nSee the n8n Workflow Patterns skill for detailed loop and nested loop patterns.\n\n### Google Sheets Node\n\n**Per-item execution**: Each input item triggers a separate API call. If you have 100 items and use a Google Sheets \"Append Row\" node, it makes 100 API calls. To write in bulk, aggregate items in a Code node first, then use a single HTTP Request with the Sheets API.\n\n**Formula columns**: Never use `append` on sheets with formula columns — it overwrites formulas. Instead, use HTTP Request with Google Sheets API `values.update` (PUT) method and a `googleApi` credential.\n\n---\n\n## Configuration Anti-Patterns\n\n### ❌ Don't: Over-configure Upfront\n\n**Bad**:\n```javascript\n// Adding every possible field\n{\n  \"method\": \"GET\",\n  \"url\": \"...\",\n  \"sendQuery\": false,\n  \"sendHeaders\": false,\n  \"sendBody\": false,\n  \"timeout\": 10000,\n  \"ignoreResponseCode\": false,\n  // ... 20 more optional fields\n}\n```\n\n**Good**:\n```javascript\n// Start minimal\n{\n  \"method\": \"GET\",\n  \"url\": \"...\",\n  \"authentication\": \"none\"\n}\n// Add fields only when needed\n```\n\n### ❌ Don't: Skip Validation\n\n**Bad**:\n```javascript\n// Configure and deploy without validating\nconst config = {...};\nn8n_update_partial_workflow({...});  // YOLO\n```\n\n**Good**:\n```javascript\n// Validate before deploying\nconst config = {...};\nconst result = validate_node({...});\nif (result.valid) {\n  n8n_update_partial_workflow({...});\n}\n```\n\n### ❌ Don't: Ignore Operation Context\n\n**Bad**:\n```javascript\n// Same config for all Slack operations\n{\n  \"resource\": \"message\",\n  \"operation\": \"post\",\n  \"channel\": \"#general\",\n  \"text\": \"...\"\n}\n\n// Then switching operation without updating config\n{\n  \"resource\": \"message\",\n  \"operation\": \"update\",  // Changed\n  \"channel\": \"#general\",  // Wrong field for update!\n  \"text\": \"...\"\n}\n```\n\n**Good**:\n```javascript\n// Check requirements when changing operation\nget_node({\n  nodeType: \"nodes-base.slack\"\n});\n// See what update operation needs (messageId, not channel)\n```\n\n---\n\n## Surgical Field Edits with patchNodeField\n\nWhen you need to edit a specific string inside a node field — rather than replacing the whole field — use `patchNodeField` in `n8n_update_partial_workflow`. This is especially useful for:\n\n- Editing code inside Code nodes without re-transmitting the full code block\n- Updating URLs or text in large HTML email templates\n- Fixing typos in JSON bodies or long text fields\n\n```javascript\n// Instead of replacing the entire jsCode field:\nn8n_update_partial_workflow({\n  id: \"wf-123\",\n  operations: [{\n    type: \"patchNodeField\",\n    nodeName: \"Code\",\n    fieldPath: \"parameters.jsCode\",\n    patches: [{find: \"const limit = 10;\", replace: \"const limit = 50;\"}]\n  }]\n})\n```\n\n`patchNodeField` is strict — it errors if the find string isn't found or matches multiple times (unless `replaceAll: true`). This prevents accidental silent failures during configuration updates. See the n8n MCP Tools Expert skill for full syntax and examples.\n\n---\n\n## Best Practices\n\n### Do\n\n1. **Start with get_node (standard detail)**\n   - ~1-2K tokens response\n   - Covers 95% of configuration needs\n   - Default detail level\n\n2. **Validate iteratively**\n   - Configure → Validate → Fix → Repeat\n   - Average 2-3 iterations is normal\n   - Read validation errors carefully\n\n3. **Use search_properties mode when stuck**\n   - If field seems missing, search for it\n   - Understand what controls field visibility\n   - `get_node({mode: \"search_properties\", propertyQuery: \"...\"})`\n\n4. **Respect operation context**\n   - Different operations = different requirements\n   - Always check get_node when changing operation\n   - Don't assume configs are transferable\n\n5. **Trust auto-sanitization**\n   - Operator structure fixed automatically\n   - Don't manually add/remove singleValue\n   - IF/Switch metadata added on save\n\n### ❌ Don't\n\n1. **Jump to detail=\"full\" immediately**\n   - Try standard detail first\n   - Only escalate if needed\n   - Full schema is 3-8K tokens\n\n2. **Configure blindly**\n   - Always validate before deploying\n   - Understand why fields are required\n   - Use search_properties for conditional fields\n\n3. **Copy configs without understanding**\n   - Different operations need different fields\n   - Validate after copying\n   - Adjust for new context\n\n4. **Manually fix auto-sanitization issues**\n   - Let auto-sanitization handle operator structure\n   - Focus on business logic\n   - Save and let system fix structure\n\n---\n\n## Silent-Failure Gotchas by Node Family\n\nSome misconfigurations pass `validate_node` and `validate_workflow` clean, run without error, and quietly do the wrong thing — `get_node` shows the fields exist but not what happens when you omit them. The high-frequency ones:\n\n- **Switch** — no `options.fallbackOutput` ⇒ unmatched items silently dropped.\n- **Merge** — `numberOfInputs` defaults to 2 (extra sources drop); `useDataOfInput` is 1-indexed vs the 0-indexed `connections.<src>.main[idx]` slot (`useDataOfInput: \"N\"` → `main[N-1]`).\n- **Database** — `{{ }}` interpolation into `parameters.query` is SQL injection; use `$1/$2` placeholders + `options.queryReplacement`.\n- **Slack** — Block Kit must be wrapped `={{ { \"blocks\": ... } }}` or it posts as plain text.\n- **Webhook / Respond** — `responseCode` defaults to 200 even on error branches.\n- **Schedule Trigger** — timezone is workflow-level (Workflow Settings), not per-rule.\n\nFull symptom/cause/fix detail (in JSON + `n8n_update_partial_workflow` terms) in **[NODE_FAMILY_GOTCHAS.md](NODE_FAMILY_GOTCHAS.md)**.\n\n---\n\n## Detailed References\n\nFor comprehensive guides on specific topics:\n\n- **[DEPENDENCIES.md](DEPENDENCIES.md)** - Deep dive into property dependencies and displayOptions\n- **[OPERATION_PATTERNS.md](OPERATION_PATTERNS.md)** - Common configuration patterns by node type\n- **[NODE_FAMILY_GOTCHAS.md](NODE_FAMILY_GOTCHAS.md)** - Silent runtime traps by family (Switch, Merge, Database, Slack, Webhook, Schedule)\n\n---\n\n## Summary\n\n**Configuration Strategy**:\n1. Start with `get_node` (standard detail is default)\n2. Configure required fields for operation\n3. Validate configuration\n4. Search properties if stuck\n5. Iterate until valid (avg 2-3 cycles)\n6. Deploy with confidence\n\n**Key Principles**:\n- **Operation-aware**: Different operations = different requirements\n- **Progressive disclosure**: Start minimal, add as needed\n- **Dependency-aware**: Understand field visibility rules\n- **Validation-driven**: Let validation guide configuration\n\n**Related Skills**:\n- **n8n MCP Tools Expert** - How to use discovery tools correctly\n- **n8n Validation Expert** - Interpret validation errors\n- **n8n Expression Syntax** - Configure expression fields\n- **n8n Workflow Patterns** - Apply patterns with proper configuration\n","frontmatter":{"name":"n8n-node-configuration","description":"Operation-aware node configuration guidance. Use when configuring nodes, understanding property dependencies, determining required fields, choosing between get_node detail levels, or learning common configuration patterns by node type. Always use this skill when setting up node parameters — it explains which fields are required for each operation, how displayOptions control field visibility, and when to use patchNodeField for surgical edits vs full node updates."},"isInternal":false,"tokens":4305,"sizeBytes":17995},{"name":"CREDENTIAL_OVERWRITES.md","path":"data/skills/n8n-self-hosting/CREDENTIAL_OVERWRITES.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-self-hosting/CREDENTIAL_OVERWRITES.md","title":"N8n-self-hosting Skill","category":"anthropic-skill","format":"markdown","content":"# N8n-self-hosting Skill\n\nPath: `data/skills/n8n-self-hosting/CREDENTIAL_OVERWRITES.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-self-hosting/CREDENTIAL_OVERWRITES.md)","tokens":300,"sizeBytes":231},{"name":"DAY2.md","path":"data/skills/n8n-self-hosting/DAY2.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-self-hosting/DAY2.md","title":"N8n-self-hosting Skill","category":"anthropic-skill","format":"markdown","content":"# N8n-self-hosting Skill\n\nPath: `data/skills/n8n-self-hosting/DAY2.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-self-hosting/DAY2.md)","tokens":300,"sizeBytes":197},{"name":"QUEUE_MODE.md","path":"data/skills/n8n-self-hosting/QUEUE_MODE.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-self-hosting/QUEUE_MODE.md","title":"N8n-self-hosting Skill","category":"anthropic-skill","format":"markdown","content":"# N8n-self-hosting Skill\n\nPath: `data/skills/n8n-self-hosting/QUEUE_MODE.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-self-hosting/QUEUE_MODE.md)","tokens":300,"sizeBytes":209},{"name":"README.md","path":"data/skills/n8n-self-hosting/README.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-self-hosting/README.md","title":"N8n-self-hosting Skill","category":"anthropic-skill","format":"markdown","content":"# N8n-self-hosting Skill\n\nPath: `data/skills/n8n-self-hosting/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-self-hosting/README.md)","tokens":300,"sizeBytes":201},{"name":"SECURITY.md","path":"data/skills/n8n-self-hosting/SECURITY.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-self-hosting/SECURITY.md","title":"N8n-self-hosting Skill","category":"anthropic-skill","format":"markdown","content":"# N8n-self-hosting Skill\n\nPath: `data/skills/n8n-self-hosting/SECURITY.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-self-hosting/SECURITY.md)","tokens":300,"sizeBytes":205},{"name":"SINGLE_MODE.md","path":"data/skills/n8n-self-hosting/SINGLE_MODE.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-self-hosting/SINGLE_MODE.md","title":"N8n-self-hosting Skill","category":"anthropic-skill","format":"markdown","content":"# N8n-self-hosting Skill\n\nPath: `data/skills/n8n-self-hosting/SINGLE_MODE.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-self-hosting/SINGLE_MODE.md)","tokens":300,"sizeBytes":211},{"name":"SKILL.md","path":"data/skills/n8n-self-hosting/SKILL.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-self-hosting/SKILL.md","title":"n8n-self-hosting","category":"anthropic-skill","format":"markdown","content":"---\nname: n8n-self-hosting\ndescription: Deploy a production self-hosted n8n end-to-end to a fresh Linux VM over SSH, using Docker Compose behind a Caddy reverse proxy with automatic HTTPS. Use whenever the user wants to self-host, install, set up, provision, or deploy n8n on their own server/VPS/box (Hetzner, DigitalOcean, AWS EC2, bare metal, etc.) — in either single/regular mode or queue mode with workers — or to update, back up, restore, or harden such an instance. This is for SELF-HOSTED n8n (Docker), not n8n Cloud and not building workflows. The skill makes the agent ask single-vs-queue first, collect the domain/SSH/timezone inputs, generate fresh secrets on the box, and bring the stack up with TLS. Trigger on \"deploy n8n\", \"self-host n8n\", \"install n8n on my server\", \"n8n docker compose\", \"n8n queue mode / workers / scaling\", \"n8n reverse proxy / SSL\", \"back up / update my n8n\", or \"we don't want to give every user the OAuth client secret\" / \"enable the Sign in with Google button\" (credential overwrites).\n---\n\n# Deploying self-hosted n8n\n\nThis skill takes a **fresh Linux VM** (Ubuntu/Debian, root or sudo SSH) to a **running,\nHTTPS, production n8n** via Docker Compose behind **Caddy** (automatic Let's Encrypt TLS).\nIt is for **self-hosted n8n on Docker** — not n8n Cloud, and not for building workflows\n(that's the rest of this pack).\n\nTwo deployment modes. The architectures differ, so **pick the mode before doing anything**.\n\nYou drive this end-to-end over SSH: preflight → install Docker → lay down the project →\ngenerate secrets → launch → verify TLS → hand off. The template files live in `assets/`;\nthe per-mode and security depth live in the reference files named below.\n\n## Rule 0 — choose the mode (ask the user)\n\nDo not guess. Ask, then commit to one:\n\n| | **Single / regular** | **Queue** |\n|---|---|---|\n| Processes | one n8n | main + N workers |\n| Extra services | none (SQLite) | Redis (queue) + Postgres (DB) |\n| Executes workflows | in the main process | on workers, in parallel |\n| Good for | 1 user, light/moderate load, simplest ops | high volume, heavy/long executions, horizontal scale |\n| Compose | `assets/docker-compose.single.yml` | `assets/docker-compose.queue.yml` |\n| Deep dive | **`SINGLE_MODE.md`** | **`QUEUE_MODE.md`** |\n\nIf unsure, start **single** — it's the simplest correct thing and covers most needs. Moving\nto queue later means swapping the compose file and migrating SQLite→Postgres, so if the user\nalready expects real volume, start **queue**.\n\n## Rule 1 — secret hygiene (non-negotiable)\n\nA misstep here leaks client credentials. Be diligent:\n\n1. **Generate every secret fresh, on the target box.** Never copy an encryption key, DB\n   password, or `.env` from another n8n instance into this one. See `SECURITY.md` for the\n   `openssl` commands.\n2. **Secrets live only in `.env`** (mode 600), referenced by the compose as `${VAR}`. Never\n   inline a secret into `docker-compose.yml`, the Caddyfile, or anything you commit.\n3. **The `N8N_ENCRYPTION_KEY` is sacred.** It encrypts every stored credential. If it's lost\n   or changes, all saved credentials become undecryptable. Set it explicitly, and tell the\n   user to back it up **off the box**. Don't echo it into long-lived logs or chat history\n   beyond what's needed to hand it over.\n4. **Never expose internal services.** Only Caddy (80/443) is public. n8n (5678), Postgres\n   (5432), Redis (6379) stay on the private Docker network — the templates already omit their\n   host port mappings. Don't add them.\n5. **`.env` and Caddy's `caddy_data` volume (the issued certs + ACME account key) are not\n   artifacts to share.** If you're working inside a git repo, confirm `.env` is git-ignored\n   before any commit.\n\n## Inputs to collect up front\n\n- **SSH target** — `user@host` and how you authenticate (key path or the user confirms the agent already has access). Root or a sudo user.\n- **Domain** — the full hostname n8n will live at, e.g. `n8n.example.com` (→ `SUBDOMAIN=n8n`, `DOMAIN_NAME=example.com`). The user must control its DNS.\n- **TLS email** — for Let's Encrypt (`SSL_EMAIL`).\n- **Timezone** — IANA name for Schedule/Cron nodes (e.g. `Europe/Warsaw`), else `Etc/UTC`.\n- **Mode** — single or queue (Rule 0). Queue → confirm the box has enough RAM (rough floor ~4 GB; each worker wants ~1–2 GB).\n- **Optional modules** — some features (currently **Agents**) are backend modules that stay off\n  unless listed in `N8N_ENABLED_MODULES`. Ask only if the user brings one up; if they do, read\n  the modules section of `QUEUE_MODE.md` before enabling it, because in queue mode it has to\n  reach the workers as well.\n\n## The deploy flow\n\nWork through these in order. `SINGLE_MODE.md` / `QUEUE_MODE.md` give the mode-specific command\ndetail; `SECURITY.md` covers secret generation and hardening; `DAY2.md` covers update/backup/restore.\n\n### 1. Preflight (the cheapest failure is the one you catch here)\n- SSH in; confirm the OS is Debian/Ubuntu-like (`. /etc/os-release`).\n- **DNS must already point at the box.** Compare the box's public IP (`curl -s ifconfig.me`)\n  with `dig +short <fqdn>` (run it from the box AND ideally your laptop). If they don't match,\n  **stop** — Caddy's ACME challenge will fail. Have the user create the A record, wait for it\n  to propagate, then continue.\n- Ports **80 and 443** must be reachable from the internet. Check the host firewall AND any\n  cloud security group / network firewall (Hetzner Cloud, AWS SG, etc.) — these are outside\n  the box and a common silent blocker.\n\n### 2. Install Docker (if absent)\n- Check `docker --version` and `docker compose version`. If missing, install Docker Engine +\n  the Compose plugin (Docker's official `get.docker.com` script on Ubuntu/Debian is fine).\n  Re-check `docker compose version` before proceeding.\n\n### 3. Lay down the project\n- Pick `DATA_FOLDER` — an **absolute path**, e.g. `/opt/n8n`. The `DATA_FOLDER` value in `.env`\n  **must equal this exact directory** (the compose mounts `${DATA_FOLDER}/caddy_config/Caddyfile`,\n  and `init-data.sh` is mounted via a relative `./` path), so always run `docker compose` from\n  here. Create it, plus `caddy_config/` and `local_files/` inside.\n- **Get the template files onto the box.** They live in this skill's `assets/` on *your* machine,\n  not on the server — transfer each one. Either `scp` them up, or (no local copy needed) write\n  each file's contents over SSH, e.g.\n  `ssh <target> 'cat > <DATA_FOLDER>/docker-compose.yml' < assets/docker-compose.single.yml`.\n  Land them with these exact names:\n  - the chosen compose → `<DATA_FOLDER>/docker-compose.yml` (rename it to exactly this)\n  - `Caddyfile` → `<DATA_FOLDER>/caddy_config/Caddyfile`\n  - **queue only:** `init-data.sh` → `<DATA_FOLDER>/init-data.sh`, then `chmod +x` it\n  - the matching `.env.*.example` → `<DATA_FOLDER>/.env`\n\n### 4. Fill `.env` + generate secrets\n- Set `DATA_FOLDER`, `DOMAIN_NAME`, `SUBDOMAIN`, `SSL_EMAIL`, `GENERIC_TIMEZONE`.\n- Generate each secret **on the box** with `openssl` (`SECURITY.md` has the commands) and **write\n  it into `.env`, replacing the matching `REPLACE_WITH_…` placeholder**: `N8N_ENCRYPTION_KEY`;\n  queue also `POSTGRES_PASSWORD` + `POSTGRES_NON_ROOT_PASSWORD`.\n- **Before launching, confirm none are left unset:** `grep REPLACE_WITH_ .env` must return nothing\n  — a leftover placeholder becomes the literal password and Postgres/n8n fail to connect.\n- `chmod 600 .env`. Record the encryption key so the user can back it up off-box.\n\n### 5. Firewall\n- `ufw`: allow OpenSSH + 80 + 443, then enable. Do **not** open 5678/5432/6379.\n\n### 6. Launch\n- `cd <DATA_FOLDER> && docker compose up -d`.\n- Queue mode brings up Redis + Postgres + main + workers (workers via `replicas`). To add\n  capacity: `docker compose up -d --scale n8n-worker=N`.\n\n### 7. Verify (don't declare success without this)\n- `docker compose ps` — every service `Up`/healthy (queue: postgres & redis `healthy` first).\n- **n8n itself up (internal):** `docker compose exec n8n wget -qO- http://localhost:5678/healthz`\n  → `{\"status\":\"ok\"}`. This separates \"n8n is running\" from \"TLS isn't ready yet.\"\n- **Cert issued:** `docker compose logs caddy | grep -i 'certificate obtained'`. First-boot ACME\n  can take a minute or two; until it finishes, a public `https://` request fails TLS — that means\n  the cert is still pending, **not** that n8n is down.\n- **Public reachability (with retry):** `curl -fsS --retry 5 --retry-delay 10 https://<fqdn>/healthz`\n  → `{\"status\":\"ok\"}`. (`/healthz` only proves the process is reachable; `/healthz/readiness`\n  additionally confirms the DB is connected and migrated — use it when debugging a boot loop.)\n- **Queue mode — main and workers must agree.** Diff their environments:\n  `diff <(docker compose exec -T n8n env | sort) <(docker compose exec -T --index 1 n8n-worker env | sort)`.\n  Only the public-URL/proxy vars should differ. Anything else means a behavioural setting reached\n  the main but not the workers — and workers are what execute workflows, so it fails at runtime\n  in one node rather than at boot. `QUEUE_MODE.md` explains the rule.\n- Open `https://<fqdn>` → the **owner setup** screen. **Whoever completes that signup form first\n  claims the instance** — an exposed un-owned instance is a race, so create the owner account\n  immediately, before sharing the URL. Enable 2FA. (Automated deploys can pre-provision the\n  owner via env vars instead — see the owner row in `SECURITY.md`.)\n\n### 8. Hand off\n- Give the user: the URL, where the project lives, the encryption key to store safely, and the\n  Day-2 basics (update / backup / restore) from **`DAY2.md`**.\n\n## What NOT to do\n\n- **Don't skip the DNS/ports preflight.** A wrong A record or a closed cloud firewall is the\n  #1 reason Caddy can't get a cert and n8n looks \"broken.\"\n- **Don't publish 5678/5432/6379** to the host. Caddy reaches n8n over the private network.\n- **Don't reuse another instance's encryption key or `.env`.** Fresh secrets per box.\n- **Don't run queue mode on SQLite.** Queue requires Postgres (the template already wires it).\n- **Don't put secrets in `docker-compose.yml` or the Caddyfile.** `.env` only.\n- **Don't add a behavioural env var to the main only (queue mode).** Modules, DB, queue,\n  binary-data and encryption settings belong in the shared `x-n8n-env` anchor so workers get\n  them too; only the public-URL/proxy vars are main-only. See `QUEUE_MODE.md`.\n- **Don't use `:latest` blindly.** Pin `N8N_IMAGE_TAG`; update deliberately (`DAY2.md`).\n\n## Reference files\n\n- **`SINGLE_MODE.md`** — single-instance specifics, SQLite vs Postgres, when to graduate to queue.\n- **`QUEUE_MODE.md`** — queue architecture, workers/concurrency/scaling, shared encryption key, the main-vs-worker **env-parity rule**, optional backend modules (`N8N_ENABLED_MODULES`, e.g. Agents), binary data (`database` mode — filesystem is unsupported in queue mode; S3/Azure = Enterprise), webhook processors, multi-main licensing.\n- **`SECURITY.md`** — generating secrets, the encryption-key rules, the full hardening checklist (telemetry off, env-access block, public API, firewall, secure cookies).\n- **`CREDENTIAL_OVERWRITES.md`** — managed OAuth: register one OAuth app instance-wide so users\n  never see a client ID/secret (\"Sign in with Google\" on self-hosted). The endpoint-vs-env choice,\n  the **mandatory** endpoint auth token, parent-type inheritance, persistence and worker reload.\n- **`DAY2.md`** — changing a setting (env var) safely, updating the image, backing up (encryption key + volume + Postgres), and restoring.\n- **`assets/`** — the templates: `docker-compose.single.yml`, `docker-compose.queue.yml`, `Caddyfile`, `.env.single.example`, `.env.queue.example`, `init-data.sh`.\n\nAuthoritative upstream reference: the official hosting docs live at\n<https://docs.n8n.io/deploy/host-n8n> (restructured mid-2026 from the old `/hosting/` paths —\nprefer these URLs). The env-var reference index is at\n<https://docs.n8n.io/deploy/host-n8n/configure-n8n/basic-configuration/use-environment-variables>.\nWhen this skill and the live docs disagree, trust the docs and tell the user.\n","frontmatter":{"name":"n8n-self-hosting","description":"Deploy a production self-hosted n8n end-to-end to a fresh Linux VM over SSH, using Docker Compose behind a Caddy reverse proxy with automatic HTTPS. Use whenever the user wants to self-host, install, set up, provision, or deploy n8n on their own server/VPS/box (Hetzner, DigitalOcean, AWS EC2, bare metal, etc.) — in either single/regular mode or queue mode with workers — or to update, back up, restore, or harden such an instance. This is for SELF-HOSTED n8n (Docker), not n8n Cloud and not building workflows. The skill makes the agent ask single-vs-queue first, collect the domain/SSH/timezone inputs, generate fresh secrets on the box, and bring the stack up with TLS. Trigger on \"deploy n8n\", \"self-host n8n\", \"install n8n on my server\", \"n8n docker compose\", \"n8n queue mode / workers / scaling\", \"n8n reverse proxy / SSL\", \"back up / update my n8n\", or \"we don't want to give every user the OAuth client secret\" / \"enable the Sign in with Google button\" (credential overwrites)."},"isInternal":false,"tokens":3212,"sizeBytes":12225},{"name":"NAMING_AND_DISCOVERY.md","path":"data/skills/n8n-subworkflows/NAMING_AND_DISCOVERY.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-subworkflows/NAMING_AND_DISCOVERY.md","title":"N8n-subworkflows Skill","category":"anthropic-skill","format":"markdown","content":"# N8n-subworkflows Skill\n\nPath: `data/skills/n8n-subworkflows/NAMING_AND_DISCOVERY.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-subworkflows/NAMING_AND_DISCOVERY.md)","tokens":300,"sizeBytes":229},{"name":"README.md","path":"data/skills/n8n-subworkflows/README.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-subworkflows/README.md","title":"N8n-subworkflows Skill","category":"anthropic-skill","format":"markdown","content":"# N8n-subworkflows Skill\n\nPath: `data/skills/n8n-subworkflows/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-subworkflows/README.md)","tokens":300,"sizeBytes":201},{"name":"SKILL.md","path":"data/skills/n8n-subworkflows/SKILL.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-subworkflows/SKILL.md","title":"n8n-subworkflows","category":"anthropic-skill","format":"markdown","content":"---\nname: n8n-subworkflows\ndescription: Build reusable, composable n8n sub-workflows. Use when extracting shared logic, building anything multi-step or reused across workflows, or any workflow over ~10 nodes — and whenever the user mentions sub-workflows, Execute Workflow, reuse, shared/common logic, modular workflows, \"Define Below\" inputs, waitForSubWorkflow, mode each vs all, or exposing a workflow as an agent tool. Covers typed sub-workflow inputs, all-vs-each execution, verb-first naming for discovery, stateless vs stateful design, and splitting by input shape.\n---\n\n# n8n Sub-workflows\n\nA sub-workflow is a reusable function. An **Execute Workflow Trigger** declares typed inputs, the body does the work, and the last node returns the output. A caller invokes it through an **Execute Workflow** node like any other step.\n\nThat framing buys you the things functions buy you everywhere: encapsulation, reuse, testability, replaceability. It's the primary reuse mechanism in n8n, and it's badly underused. Without it, the same logic gets copy-pasted across workflows — then a bug gets fixed in two places, the third copy gets missed, and your \"identical\" copies quietly drift apart.\n\nThis skill is about when to reach for a sub-workflow, how to define its input/output contract so callers (and agents) can actually use it, how to call it correctly (`all` vs `each`, blocking vs fire-and-forget), and how to name it so it gets found instead of rebuilt.\n\n---\n\n## The two non-negotiables\n\nEverything else is judgement. These two are not.\n\n### 1. Search before you build\n\nBefore you write logic for a generic problem, check whether a sub-workflow already does it. The community MCP can't filter workflows by tag, so the **name is the discovery surface**:\n\n```\nn8n_list_workflows()                          # scan the library\nn8n_get_workflow({ id: \"<candidate>\" })       # read its inputs/outputs + body\n```\n\nIf something fits, use it and tell the user (\"I found `Subworkflow: Parse RFC2822 date` — using that\"). If nothing fits, build it *with a discoverable name* so the next search finds it. The discovery convention (verb-first prefixes) lives in **NAMING_AND_DISCOVERY.md**.\n\n### 2. The Execute Workflow Trigger uses \"Define Below\" with typed fields — not passthrough\n\nThe trigger has two input modes. **Default to \"Define Below\"** with explicit typed fields. Define Below is the only mode that gives callers a schema to fill — it's what lets an AI agent pass values via `$fromAI` and what lets structured callers map fields cleanly. Passthrough has no schema, so the trigger can't be wired as a clean agent tool and structured callers have nothing to bind to.\n\nTwo exceptions, and only two:\n\n- **Binary input.** Typed fields are JSON-only. If the sub-workflow must receive an image/file/PDF, you need passthrough so the `binary` slot flows through.\n- **Zero inputs.** Define Below requires at least one field. A genuinely no-arg operation (\"list active credentials\", \"current count\") has nowhere to put an empty schema, so passthrough is the only option.\n\nOutside those two cases, passthrough is a bug. See \"Inputs and outputs as a contract\" below.\n\n---\n\n## Should this be a sub-workflow?\n\nYou're about to write a chunk of logic. Run it through this:\n\n```\nCould this plausibly be needed in another workflow?\n  └─ Yes → extract.\n\nIs it a generic concern (auth, retry, parsing, formatting, ID generation)?\n  └─ Almost always → extract. These are the canonical reusable sub-workflows.\n\nIs it >5 nodes and conceptually one thing?\n  └─ Probably extract, even if reuse isn't certain. It's better isolated.\n\nIs it one HTTP call with no logic around it?\n  └─ Don't. A sub-workflow that's just trigger → HTTP → return adds a boundary\n     for nothing.\n\nIs it tightly coupled to this one caller's data shape?\n  └─ Don't extract yet — fix the data shape first, or you just relocate the coupling.\n```\n\nThe reasons to extract go beyond reuse:\n\n- **Readability.** The caller shows one node (\"Parse date\") instead of five.\n- **Testability.** Run the sub-workflow alone with pinned input: `n8n_test_workflow({workflowId, method: \"prepare\"})` names the nodes that need data, then `method: \"pinned\"` runs it with what you build (needs `N8N_MCP_ACCESS_TOKEN` and the workflow's \"Available in MCP\" setting — see **n8n-mcp-tools-expert**). A sub-workflow has no HTTP trigger, so the default `method: \"auto\"` cannot run it.\n- **Replaceability.** Swap the implementation without rippling to callers.\n\nA 20-node workflow is fine *if it's mostly a linear sequence of Execute Workflow calls and decisions* — each node has one purpose, and you inspect a section by opening the sub-workflow it calls. A 20-node workflow of inline transformations is not fine. If yours has 15+ nodes and isn't mostly sub-workflow calls and branches, extract more.\n\n---\n\n## Stateless vs. stateful (deliberately)\n\nBoth are first-class. The choice is about intent and what the contract promises.\n\n**Stateless** — input in, output out, no I/O beyond that. The default for pure logic. When you need it again, you call it without worrying about side effects firing.\n\n- `Subworkflow: Parse RFC2822 date` — date string → ISO date or error.\n- `Subworkflow: Compute MRR from subscription` — subscription object → number.\n- `Subworkflow: Format invoice as HTML` — invoice data → HTML string.\n\n**Stateful (deliberate)** — reads or writes external state *behind a clean contract*. This is the repository pattern: the sub-workflow abstracts the storage operation so callers think in domain terms, not SQL.\n\n- `Customer: get by id` — id → customer object or `{ ok: false, error: \"not_found\" }`. Reads the DB.\n- `Customer: write billing record` — record → `{ ok: true, id }`. Writes the DB.\n- `Notify: send to on-call` — channel, message → `{ ok: true, messageId }`. Calls Slack/SMTP.\n\nWhy build these as sub-workflows: callers think `get customer by id` instead of writing the query; you can swap the store (Postgres → Supabase, native node → HTTP) without touching a single caller; and idempotency, retry, and validation get centralized in one place.\n\nWhat to avoid is **accidental state** — a sub-workflow named and described as pure that quietly writes to a log table. That ambushes every caller who reasonably assumed it was safe to retry or compose. Either make the side effect part of the contract (rename it, document it, return its result) or move it out.\n\n---\n\n## Inputs and outputs as a contract\n\nThe trigger's declared fields and the last node's output shape *are* the sub-workflow's API. Treat them like one.\n\n### Declaring typed inputs (Define Below)\n\nEach declared input is a typed parameter the caller fills. Pick types deliberately (`string`, `number`, `boolean`, `array`, `object`) — an agent uses these as the required types when filling tool parameters, and humans rely on them when wiring callers. The trigger node parameters look like this:\n\n```json\n{\n  \"type\": \"n8n-nodes-base.executeWorkflowTrigger\",\n  \"parameters\": {\n    \"workflowInputs\": {\n      \"values\": [\n        { \"name\": \"list_of_ids\",        \"type\": \"array\" },\n        { \"name\": \"include_transcript\", \"type\": \"boolean\" },\n        { \"name\": \"session_id\",          \"type\": \"string\" }\n      ]\n    }\n  }\n}\n```\n\nInside the body, read them as `$json.list_of_ids`, or from anywhere downstream as `$('When Executed by Another Workflow').first().json.<field>` (see **n8n-expression-syntax**).\n\n### The contract rules\n\n- **Document inputs and outputs in the workflow `description`.** Field names, types, purpose, and a few representative keywords. The description is what callers (human and agent) read for the contract, and it's what `n8n_list_workflows` matches against.\n- **Return consistent, natural shapes — not storage shapes.** A sub-workflow that owns a Data Table or an S3 file hides that representation from callers. Arrays return as arrays, objects as objects, dates as ISO strings — regardless of whether the underlying storage was JSON-stringified text. The return contract is the *interface*; the storage layout is *implementation detail*. Common slip: a sub-workflow with a \"fresh\" path (just-computed, natural shape) and a \"cached\" path (just read from a stringified column). Wrong instinct: stringify the fresh path to match the cached one. Right instinct: parse the cached path so both return the natural shape.\n- **Return errors, don't always throw.** For *expected* failures (a parse error, a not-found), return `{ ok: false, error: \"...\" }` so the caller can branch without wiring an error output. Reserve throwing for genuinely unexpected failures — see **n8n-error-handling**.\n- **The contract is frozen once it has callers.** Adding *optional* fields is safe. Renaming or removing a field is dangerous: n8n won't error on an unrecognized input field — the body just sees `undefined`, the caller has no idea, and you get a silent contract break. To change a field, enumerate every caller (`n8n_list_workflows` + inspect each one's Execute Workflow node), migrate them in the same change, and verify with `validate_workflow` and `n8n_get_workflow` before you're done.\n\n### The final Return node — the legitimate Set exception\n\nShape the output with a final **Set / Edit Fields** node, named `Return` or `Return <thing>`. This is the one place a Set node earns its keep against the usual \"don't add a trailing Set node\" advice from **n8n-expression-syntax**: the implicit consumer of a sub-workflow's last node is *every caller*, so an explicit Set makes the return contract visible — a reader sees the whole API by reading one node, and you strip any noise fields the last computation node carried.\n\n---\n\n## Calling sub-workflows: `mode` and `waitForSubWorkflow`\n\nTwo settings on the caller's **Execute Workflow** node decide how the sub-workflow runs.\n\n### `mode`: `all` vs `each`\n\n| `mode` | Sub-workflow runs | Items per run |\n|---|---|---|\n| `all` (default) | once | all N items (flowing per-item through nodes as usual) |\n| `each` | N times | exactly one item per run |\n\nFor a body that just processes items the normal way, the two are equivalent — n8n nodes iterate per-item either way. **The split only matters when the body assumes it sees exactly one item**: a per-run aggregation, \"this is THE customer to act on\" logic, or a final write that should fire once per input. With `all`, that body gets all N items at once and the assumption breaks (you aggregate everyone into one result instead of one-per-input). With `each`, each invocation gets one item and the assumption holds.\n\nSo: when you need per-item iteration, prefer `mode: each` over dropping a Loop Over Items node *inside* the sub-workflow. The mode does the iteration for you, and the body stays simple and single-item.\n\n### `waitForSubWorkflow`: `true` vs `false`\n\n`waitForSubWorkflow` defaults to `true` — the caller blocks until the sub-workflow returns, then continues with its output. Set `options.waitForSubWorkflow: false` to fire-and-forget: the call dispatches, the caller moves on immediately, the sub-workflow runs in the background, and downstream sees no return data.\n\n### The only true parallelization n8n offers\n\n`mode: each` + `waitForSubWorkflow: false` is **the only way to get genuinely concurrent sub-workflow execution**: N items dispatch N runs that execute in parallel (still bounded by per-instance concurrency limits). The caller doesn't know when — or whether — any of them finished, so it's only useful with a separate completion-tracking mechanism, typically a Data Table the sub-workflow updates as it progresses. The full stage → dispatch → poll pattern is in **SUBWORKFLOW_PATTERNS.md** (\"Fire-and-forget parallelization\").\n\n---\n\n## Splitting by input shape (the N+1 pattern)\n\nWhen a sub-workflow has multiple input paths whose contracts *genuinely* differ — binary vs JSON, sync vs async, divergent auth schemes — don't cram them under one trigger with passthrough + an internal Switch. The forcing function is real: passthrough (for binary or zero-input) and Define Below (for typed inputs) are mutually exclusive on a single trigger. The reflex to \"pick passthrough because it's most permissive, then branch inside\" costs you the typed schema (no clean agent tool), grows branch-shape cruft, and turns every new input shape into more branching.\n\nThe fix: for N divergent input contracts, build **N+1 sub-workflows** — one outer per contract, each doing its input-specific prep (validation, fetching, hashing, extraction) and calling **one shared downstream** sub-workflow with a normalized shape. The shared core has a single typed input contract and knows nothing about which outer called it. The worked example (process a paper from an external ID *or* an uploaded PDF) is in **SUBWORKFLOW_PATTERNS.md**.\n\n---\n\n## Sub-workflow as an agent tool\n\nA sub-workflow with a typed Define Below trigger doubles as an AI-agent tool: the agent fills the declared fields via `$fromAI`, the body runs, the result comes back as the tool observation. This is the high-value reason to default to Define Below — passthrough triggers can't expose a fill-able schema.\n\nThe zero-input case still works as a tool: the agent's only decision is whether to invoke. The binary case does *not* wire cleanly as a tool, because agents can't pass binary directly.\n\nFor tool naming, descriptions, and the binary-input workaround, see **n8n-agents**; for the binary handling itself, **n8n-binary-and-data**.\n\n---\n\n## Anti-patterns\n\n| Anti-pattern | What goes wrong | Fix |\n|---|---|---|\n| Duplicating the same logic in three workflows | A bug gets fixed in two places, the third drifts | Extract once to a named sub-workflow |\n| Building a new sub-workflow without searching | The library grows duplicates; future searches find both | `n8n_list_workflows` / `n8n_get_workflow` first |\n| Trigger set to passthrough when not handling binary and not zero-input | No schema → agents can't fill params, structured callers can't bind | Use Define Below with typed `workflowInputs.values` |\n| Zero-input passthrough with no clear-and-document | Body silently reads stray fields from whatever the caller forwarded | Start with a Set (\"Keep Only Set\", no fields) and a sticky noting \"no inputs expected\" |\n| Sub-workflow named/described as pure that quietly writes state | Callers can't reason about retry/idempotency; the side effect ambushes them | Make the side effect part of the contract, or move it out |\n| Sub-workflow with no `description` | Won't be found in future searches; nobody knows what it does | Set `description` with input/output shape + keywords |\n| Name like `Helper 3` / no prefix | Doesn't say what it does, matches no prefix search | Verb-first prefix (`Subworkflow:`, `<Domain>:`, `Tool:`) |\n| `mode: all` on a body that assumes one item | Aggregates all inputs into one result instead of one-per-input | `mode: each` (and skip the internal Loop Over Items) |\n| Renaming a live input field without migrating callers | Callers send the old name → body sees `undefined`, no error anywhere | Migrate every caller in the same change; verify with `validate_workflow` |\n| 30-node workflow with no extraction | Hard to read, test, and replace | Extract logical sections into sub-workflows |\n\n---\n\n## What's NOT available via the community MCP\n\n| Want to do | Reality |\n|---|---|\n| Filter/discover workflows by **tag** | The MCP can't read or filter by tags (UI-only). Discovery is the *name* — use verb-first prefixes and `n8n_list_workflows`. |\n| Catch an **unrecognized input field** | n8n doesn't error on one. The body sees `undefined` and the caller never knows — a silent contract break. Verify field renames by hand across callers. |\n| Set the input mode / fields without a typed trigger | The trigger node itself must declare `workflowInputs.values`. Configure it with `n8n_update_partial_workflow` (`updateNode` / `patchNodeField`); validate with `get_node` / `validate_node`. |\n\nWhat the MCP **can** do: build the sub-workflow and its callers (`n8n_update_partial_workflow` with `addNode` / `addConnection` / `updateNode` / `patchNodeField`), discover existing ones (`n8n_list_workflows`, `n8n_get_workflow`), validate (`validate_workflow`, `n8n_validate_workflow`), test in isolation (`n8n_test_workflow`), inspect runs (`n8n_executions`), back a stateful sub-workflow with a Data Table (`n8n_manage_datatable`), and activate (`activateWorkflow`).\n\n---\n\n## Reference files\n\n| File | Read when |\n|---|---|\n| **SUBWORKFLOW_PATTERNS.md** | `mode: all` vs `each` in depth, splitting by input shape (the N+1 worked example), fire-and-forget parallelization with Data Table polling |\n| **NAMING_AND_DISCOVERY.md** | Naming a new sub-workflow, the verb-first prefix convention, searching for existing ones, writing a discoverable description |\n\n---\n\n## Integration with other skills\n\n- **n8n-workflow-patterns** — use it for the overall shape of the orchestrating workflow; use this skill to decide which sections become sub-workflows.\n- **n8n-mcp-tools-expert** — parameter formats for `n8n_list_workflows`, `n8n_get_workflow`, `n8n_update_partial_workflow`, and `n8n_manage_datatable` (the Data Table behind a stateful sub-workflow and the fire-and-forget poll).\n- **n8n-node-configuration** — `workflowInputs` and the `inputSource` (Define Below vs passthrough) toggle are displayOptions-driven config on the Execute Workflow Trigger.\n- **n8n-expression-syntax** — reading inputs (`$json`, `$('When Executed by Another Workflow')`) and the legitimate final-Set exception both live here.\n- **n8n-error-handling** — expected failures return `{ ok: false, error }`; unexpected ones throw and route through error outputs. A sub-workflow boundary is a natural place to define that line.\n- **n8n-validation-expert** — validate the sub-workflow and its callers; an unrecognized input field won't surface here, so verify field changes manually.\n- **n8n-code-javascript / n8n-code-python** — when a sub-workflow's body is a single Code node, its contract is still the trigger's typed inputs and the returned shape, not the Code node's internals.\n- **n8n-code-tool** — the Custom Code Tool is the *inline* agent-tool option; a sub-workflow tool is the reusable, multi-step one. Pick the sub-workflow when the logic is shared across agents or needs the full Code-node sandbox.\n- **n8n-agents** — wiring a typed sub-workflow as an agent tool, including the zero-input and binary cases.\n- **n8n-binary-and-data** — passthrough triggers for binary input, and why binary can't flow through an agent tool directly.\n- **using-n8n-mcp-skills** — when to consult which skill across a build.\n\n---\n\n## Quick reference checklist\n\nBefore shipping a sub-workflow:\n\n- [ ] **Searched first** with `n8n_list_workflows` / `n8n_get_workflow` — it doesn't already exist\n- [ ] **Trigger uses Define Below** with typed `workflowInputs.values` (unless binary or zero-input)\n- [ ] **Zero-input passthrough** (if used) starts with a \"Keep Only Set\" Set node + a sticky noting no inputs\n- [ ] **Name** has a verb-first prefix (`Subworkflow:`, `<Domain>:`, `Tool:`)\n- [ ] **Description** documents input/output shape and carries searchable keywords\n- [ ] **Returns a natural, consistent shape** via a final `Return` Set node — not a storage shape\n- [ ] **Expected failures** return `{ ok: false, error }`; only unexpected ones throw\n- [ ] **Caller `mode`** is `each` if the body assumes a single item (not an internal Loop Over Items)\n- [ ] **`waitForSubWorkflow`** is set deliberately (`false` only with a completion-tracking mechanism)\n- [ ] **Stateful sub-workflows** declare their side effect in name + description — no accidental state\n- [ ] **Validated** with `validate_workflow`; tested in isolation with `n8n_test_workflow`\n\n---\n\n**Remember**: a sub-workflow is a function. Its API is the trigger's typed inputs and the last node's output shape — make both explicit, name it so it's found, and call it with the `mode` its body expects. A passthrough trigger that isn't for binary or a zero-arg op, or a name nobody can search, is how a reusable function quietly becomes the next duplicate.\n","frontmatter":{"name":"n8n-subworkflows","description":"Build reusable, composable n8n sub-workflows. Use when extracting shared logic, building anything multi-step or reused across workflows, or any workflow over ~10 nodes — and whenever the user mentions sub-workflows, Execute Workflow, reuse, shared/common logic, modular workflows, \"Define Below\" inputs, waitForSubWorkflow, mode each vs all, or exposing a workflow as an agent tool. Covers typed sub-workflow inputs, all-vs-each execution, verb-first naming for discovery, stateless vs stateful design, and splitting by input shape."},"isInternal":false,"tokens":4723,"sizeBytes":20249},{"name":"SUBWORKFLOW_PATTERNS.md","path":"data/skills/n8n-subworkflows/SUBWORKFLOW_PATTERNS.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-subworkflows/SUBWORKFLOW_PATTERNS.md","title":"N8n-subworkflows Skill","category":"anthropic-skill","format":"markdown","content":"# N8n-subworkflows Skill\n\nPath: `data/skills/n8n-subworkflows/SUBWORKFLOW_PATTERNS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-subworkflows/SUBWORKFLOW_PATTERNS.md)","tokens":300,"sizeBytes":229},{"name":"ERROR_CATALOG.md","path":"data/skills/n8n-validation-expert/ERROR_CATALOG.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-validation-expert/ERROR_CATALOG.md","title":"N8n-validation-expert Skill","category":"anthropic-skill","format":"markdown","content":"# N8n-validation-expert Skill\n\nPath: `data/skills/n8n-validation-expert/ERROR_CATALOG.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-validation-expert/ERROR_CATALOG.md)","tokens":300,"sizeBytes":230},{"name":"FALSE_POSITIVES.md","path":"data/skills/n8n-validation-expert/FALSE_POSITIVES.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-validation-expert/FALSE_POSITIVES.md","title":"N8n-validation-expert Skill","category":"anthropic-skill","format":"markdown","content":"# N8n-validation-expert Skill\n\nPath: `data/skills/n8n-validation-expert/FALSE_POSITIVES.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-validation-expert/FALSE_POSITIVES.md)","tokens":300,"sizeBytes":234},{"name":"README.md","path":"data/skills/n8n-validation-expert/README.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-validation-expert/README.md","title":"N8n-validation-expert Skill","category":"anthropic-skill","format":"markdown","content":"# N8n-validation-expert Skill\n\nPath: `data/skills/n8n-validation-expert/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-validation-expert/README.md)","tokens":300,"sizeBytes":216},{"name":"REVIEW_CHECKLIST.md","path":"data/skills/n8n-validation-expert/REVIEW_CHECKLIST.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-validation-expert/REVIEW_CHECKLIST.md","title":"N8n-validation-expert Skill","category":"anthropic-skill","format":"markdown","content":"# N8n-validation-expert Skill\n\nPath: `data/skills/n8n-validation-expert/REVIEW_CHECKLIST.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-validation-expert/REVIEW_CHECKLIST.md)","tokens":300,"sizeBytes":236},{"name":"SKILL.md","path":"data/skills/n8n-validation-expert/SKILL.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-validation-expert/SKILL.md","title":"n8n-validation-expert","category":"anthropic-skill","format":"markdown","content":"# n8n-validation-expert\n\nPath: `data/skills/n8n-validation-expert/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-validation-expert/SKILL.md)","tokens":300,"sizeBytes":208},{"name":"ai_agent_workflow.md","path":"data/skills/n8n-workflow-patterns/ai_agent_workflow.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-workflow-patterns/ai_agent_workflow.md","title":"N8n-workflow-patterns Skill","category":"anthropic-skill","format":"markdown","content":"# N8n-workflow-patterns Skill\n\nPath: `data/skills/n8n-workflow-patterns/ai_agent_workflow.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-workflow-patterns/ai_agent_workflow.md)","tokens":300,"sizeBytes":238},{"name":"database_operations.md","path":"data/skills/n8n-workflow-patterns/database_operations.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-workflow-patterns/database_operations.md","title":"N8n-workflow-patterns Skill","category":"anthropic-skill","format":"markdown","content":"# N8n-workflow-patterns Skill\n\nPath: `data/skills/n8n-workflow-patterns/database_operations.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-workflow-patterns/database_operations.md)","tokens":300,"sizeBytes":242},{"name":"http_api_integration.md","path":"data/skills/n8n-workflow-patterns/http_api_integration.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-workflow-patterns/http_api_integration.md","title":"N8n-workflow-patterns Skill","category":"anthropic-skill","format":"markdown","content":"# N8n-workflow-patterns Skill\n\nPath: `data/skills/n8n-workflow-patterns/http_api_integration.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-workflow-patterns/http_api_integration.md)","tokens":300,"sizeBytes":244},{"name":"README.md","path":"data/skills/n8n-workflow-patterns/README.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-workflow-patterns/README.md","title":"N8n-workflow-patterns Skill","category":"anthropic-skill","format":"markdown","content":"# N8n-workflow-patterns Skill\n\nPath: `data/skills/n8n-workflow-patterns/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-workflow-patterns/README.md)","tokens":300,"sizeBytes":216},{"name":"scheduled_tasks.md","path":"data/skills/n8n-workflow-patterns/scheduled_tasks.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-workflow-patterns/scheduled_tasks.md","title":"N8n-workflow-patterns Skill","category":"anthropic-skill","format":"markdown","content":"# N8n-workflow-patterns Skill\n\nPath: `data/skills/n8n-workflow-patterns/scheduled_tasks.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-workflow-patterns/scheduled_tasks.md)","tokens":300,"sizeBytes":234},{"name":"SKILL.md","path":"data/skills/n8n-workflow-patterns/SKILL.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-workflow-patterns/SKILL.md","title":"n8n-workflow-patterns","category":"anthropic-skill","format":"markdown","content":"# n8n-workflow-patterns\n\nPath: `data/skills/n8n-workflow-patterns/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-workflow-patterns/SKILL.md)","tokens":300,"sizeBytes":208},{"name":"webhook_processing.md","path":"data/skills/n8n-workflow-patterns/webhook_processing.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-workflow-patterns/webhook_processing.md","title":"N8n-workflow-patterns Skill","category":"anthropic-skill","format":"markdown","content":"# N8n-workflow-patterns Skill\n\nPath: `data/skills/n8n-workflow-patterns/webhook_processing.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-workflow-patterns/webhook_processing.md)","tokens":300,"sizeBytes":240},{"name":"README.md","path":"data/skills/using-n8n-mcp-skills/README.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/using-n8n-mcp-skills/README.md","title":"Using-n8n-mcp-skills Skill","category":"anthropic-skill","format":"markdown","content":"# Using-n8n-mcp-skills Skill\n\nPath: `data/skills/using-n8n-mcp-skills/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/using-n8n-mcp-skills/README.md)","tokens":300,"sizeBytes":213},{"name":"SKILL.md","path":"data/skills/using-n8n-mcp-skills/SKILL.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/using-n8n-mcp-skills/SKILL.md","title":"using-n8n-mcp-skills","category":"anthropic-skill","format":"markdown","content":"# using-n8n-mcp-skills\n\nPath: `data/skills/using-n8n-mcp-skills/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/using-n8n-mcp-skills/SKILL.md)","tokens":300,"sizeBytes":205},{"name":"CLAUDE.md","path":"CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/CLAUDE.md","title":"Claude Agent Guidelines & System Prompt","category":"claude-rule","format":"markdown","content":"# Claude Agent Guidelines & System Prompt\n\nPath: `CLAUDE.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/CLAUDE.md)","tokens":300,"sizeBytes":160},{"name":"code-reviewer.md","path":".claude/agents/code-reviewer.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/.claude/agents/code-reviewer.md","title":"Subagent: code-reviewer","category":"subagent-persona","format":"markdown","content":"# Subagent: code-reviewer\n\nPath: `.claude/agents/code-reviewer.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/.claude/agents/code-reviewer.md)","tokens":300,"sizeBytes":188},{"name":"context-manager.md","path":".claude/agents/context-manager.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/.claude/agents/context-manager.md","title":"Subagent: context-manager","category":"subagent-persona","format":"markdown","content":"# Subagent: context-manager\n\nPath: `.claude/agents/context-manager.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/.claude/agents/context-manager.md)","tokens":300,"sizeBytes":194},{"name":"debugger.md","path":".claude/agents/debugger.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/.claude/agents/debugger.md","title":"Subagent: debugger","category":"subagent-persona","format":"markdown","content":"# Subagent: debugger\n\nPath: `.claude/agents/debugger.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/.claude/agents/debugger.md)","tokens":300,"sizeBytes":173},{"name":"deployment-engineer.md","path":".claude/agents/deployment-engineer.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/.claude/agents/deployment-engineer.md","title":"Subagent: deployment-engineer","category":"subagent-persona","format":"markdown","content":"# Subagent: deployment-engineer\n\nPath: `.claude/agents/deployment-engineer.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/.claude/agents/deployment-engineer.md)","tokens":300,"sizeBytes":206},{"name":"mcp-backend-engineer.md","path":".claude/agents/mcp-backend-engineer.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/.claude/agents/mcp-backend-engineer.md","title":"Subagent: mcp-backend-engineer","category":"subagent-persona","format":"markdown","content":"# Subagent: mcp-backend-engineer\n\nPath: `.claude/agents/mcp-backend-engineer.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/.claude/agents/mcp-backend-engineer.md)","tokens":300,"sizeBytes":209},{"name":"n8n-mcp-tester.md","path":".claude/agents/n8n-mcp-tester.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/.claude/agents/n8n-mcp-tester.md","title":"Subagent: n8n-mcp-tester","category":"subagent-persona","format":"markdown","content":"# Subagent: n8n-mcp-tester\n\nPath: `.claude/agents/n8n-mcp-tester.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/.claude/agents/n8n-mcp-tester.md)","tokens":300,"sizeBytes":191},{"name":"technical-researcher.md","path":".claude/agents/technical-researcher.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/.claude/agents/technical-researcher.md","title":"Subagent: technical-researcher","category":"subagent-persona","format":"markdown","content":"# Subagent: technical-researcher\n\nPath: `.claude/agents/technical-researcher.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/.claude/agents/technical-researcher.md)","tokens":300,"sizeBytes":209},{"name":"test-automator.md","path":".claude/agents/test-automator.md","rawUrl":"https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/.claude/agents/test-automator.md","title":"Subagent: test-automator","category":"subagent-persona","format":"markdown","content":"# Subagent: test-automator\n\nPath: `.claude/agents/test-automator.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/.claude/agents/test-automator.md)","tokens":300,"sizeBytes":191}],"systemPromptSnippet":"<yakaai_skills repo=\"czlonkowski/n8n-mcp\">\n<!-- File: AGENTS.md (Tokens: ~300 | Category: root-instruction) -->\n# AI Agent Protocol & Instructions\n\nPath: `AGENTS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/AGENTS.md)\n\n<!-- File: data/skills/n8n-agents/CHAT_AGENT_PATTERNS.md (Tokens: ~300 | Category: anthropic-skill) -->\n# N8n-agents Skill\n\nPath: `data/skills/n8n-agents/CHAT_AGENT_PATTERNS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-agents/CHAT_AGENT_PATTERNS.md)\n\n<!-- File: data/skills/n8n-agents/EXAMPLES.md (Tokens: ~300 | Category: anthropic-skill) -->\n# N8n-agents Skill\n\nPath: `data/skills/n8n-agents/EXAMPLES.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-agents/EXAMPLES.md)\n\n<!-- File: data/skills/n8n-agents/HUMAN_REVIEW.md (Tokens: ~300 | Category: anthropic-skill) -->\n# N8n-agents Skill\n\nPath: `data/skills/n8n-agents/HUMAN_REVIEW.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-agents/HUMAN_REVIEW.md)\n\n<!-- File: data/skills/n8n-agents/MEMORY.md (Tokens: ~300 | Category: anthropic-skill) -->\n# N8n-agents Skill\n\nPath: `data/skills/n8n-agents/MEMORY.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-agents/MEMORY.md)\n\n<!-- File: data/skills/n8n-agents/RAG.md (Tokens: ~300 | Category: anthropic-skill) -->\n# N8n-agents Skill\n\nPath: `data/skills/n8n-agents/RAG.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-agents/RAG.md)\n\n<!-- File: data/skills/n8n-agents/README.md (Tokens: ~300 | Category: anthropic-skill) -->\n# N8n-agents Skill\n\nPath: `data/skills/n8n-agents/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-agents/README.md)\n\n<!-- File: data/skills/n8n-agents/SKILL.md (Tokens: ~6623 | Category: anthropic-skill) -->\n---\nname: n8n-agents\ndescription: Design n8n AI agents the right way. Use when building or editing any @n8n/n8n-nodes-langchain.* AI node — an AI Agent, LLM chain, Text Classifier, or Information Extractor — and whenever the user mentions AI agents, LLM with tools, tool calling, $fromAI, system prompts, agent memory, sessionId, structured/JSON output, output parser, RAG, vector store, a chat assistant/bot, or human-in-the-loop review. Covers Agent-vs-chain-vs-classifier choice, the model/memory/tools/outputParser slots, tool names/descriptions as prompt, structured output with autoFix, memory, RAG, human review, and chat topologies.\n---\n\n# n8n Agents\n\nThe n8n AI Agent node (`@n8n/n8n-nodes-langchain.agent`) is a multi-turn LLM driver with sub-nodes for the model, memory, tools, and an optional output parser. This skill is the **deep** guide to designing agents and the LangChain family around them. For the high-level \"where an agent fits in a workflow\" picture, see **n8n-workflow-patterns** `ai_agent_workflow.md` — this skill goes one level down into *how to build it well*.\n\nFor node-type formats: in workflow JSON the LangChain nodes use the long `@n8n/n8n-nodes-langchain.*` form (`.agent`, `.lmChatOpenAi`, `.memoryBufferWindow`, `.outputParserStructured`, `.toolWorkflow`, `.toolHttpRequest`, `.toolCode`). When you call `get_node` / `validate_node`, use the **short** form (`nodes-langchain.agent`). See **n8n-mcp-tools-expert** for the format rules.\n\n---\n\n## Pick the right node first\n\nReaching for an Agent when the task is one-shot classification or extraction is the most common over-build. Decide before you wire anything:\n\n| You need to… | Use | Why |\n|---|---|---|\n| Call tools, reason over multiple turns, or hold memory | **AI Agent** (`.agent`) | The full loop: model + tools + memory + optional parser. Also a fine default when you'd rather standardize. |\n| One-shot text in → text out, no tools | **Basic LLM Chain** (`.chainLlm`) | No agent loop, easier to debug. Still accepts an `outputParserStructured` sub-node. |\n| Route a natural-language input to one of **N branches** | **Text Classifier** (`.textClassifier`) | ONE node, N output handles, downstream wires directly into each. Not Agent + Switch. |\n| Pull structured fields out of free text | **Information Extractor** (`.informationExtractor`) | Purpose-built field extraction with a schema. |\n| 3-way positive/neutral/negative split | **Sentiment Analysis** (`.sentimentAnalysis`) | Built-in branch outputs. |\n| Condense a long document | **Summarization Chain** (`.chainSummarization`) | Map-reduce summarization built in. |\n| Generate an image / audio / video | **The provider's native single-call node** (OpenAI, Gemini, ElevenLabs…) | NEVER wrap media generation in an Agent — see \"Binary and the agent boundary\". |\n\n**Text Classifier detail (the Agent + Switch anti-pattern):** every category needs both a **name AND a description**. The model routes against the *description*, not the name — a category with no description gets picked by coin-flip. Set `options.enableAutoFixing: true` for robustness on edge inputs. One node, N branches, done. Reaching for an Agent that \"decides\" then a Switch that \"routes\" is two nodes plus prompt boilerplate for what Text Classifier does natively.\n\nChat-model nodes (`.lmChatOpenAi`, `.lmChatAnthropic`, `.lmChatOpenRouter`, …) are **sub-nodes** — they don't run standalone. They wire into a chain, agent, classifier, or extractor via the `ai_languageModel` connection.\n\n---\n\n## The sub-node pattern\n\nThe Agent has a **main input** (the prompt / user message) and up to four **sub-node slots**, each wired by its own `ai_*` connection type:\n\n| Slot | Connection type | Required? | Node example |\n|---|---|---|---|\n| **model** | `ai_languageModel` | Yes | `.lmChatOpenAi`, `.lmChatAnthropic`, `.lmChatOpenRouter` |\n| **memory** | `ai_memory` | Optional | `.memoryBufferWindow`, `.memoryPostgresChat` |\n| **tools** | `ai_tool` | Optional (but the point of an agent) | `slackTool`, `.toolWorkflow`, `.toolHttpRequest`, `.toolCode` |\n| **outputParser** | `ai_outputParser` | Optional | `.outputParserStructured` |\n\nA sub-node connects FROM itself TO the agent. In workflow JSON the connection lives on the **sub-node**, keyed by the `ai_*` type:\n\n```json\n\"Main LLM\": {\n  \"ai_languageModel\": [[{ \"node\": \"AI Agent\", \"type\": \"ai_languageModel\", \"index\": 0 }]]\n},\n\"Simple Memory\": {\n  \"ai_memory\": [[{ \"node\": \"AI Agent\", \"type\": \"ai_memory\", \"index\": 0 }]]\n},\n\"Search customer DB\": {\n  \"ai_tool\": [[{ \"node\": \"AI Agent\", \"type\": \"ai_tool\", \"index\": 0 }]]\n}\n```\n\nMultiple tools all connect into the same `ai_tool` index 0 — they stack, they don't fan into separate indices. With `n8n_update_partial_workflow` you wire each with an `addConnection` op using `sourceOutput: \"ai_tool\"`. The agent puts its final answer in **`$json.output`** (not `.text`, not `.response`) — downstream nodes read `{{ $json.output }}`.\n\nSee **EXAMPLES.md** for a complete stateless agent-core node-object snippet.\n\n---\n\n## Two non-negotiables\n\n1. **Tool names and descriptions ARE part of the prompt.** The model picks a tool by reading its name and description — nothing else. A tool named `tool1` with an empty description is invisible to the model: it skips it, mis-selects it, or hallucinates parameters. There's usually no error — just an agent that \"won't use my tool\". Treat both like API design. → **TOOLS.md**\n2. **Structured output must parse AND autoFix.** An `outputParserStructured` with `autoFix: true` and a **coding-capable fixer model** is the production pattern. Without autoFix, one malformed JSON response halts the whole workflow. → **STRUCTURED_OUTPUT.md**\n\n---\n\n## Strong defaults\n\n- **Per-tool usage goes in the tool description, not the system prompt.** Anything about *how to call this specific tool* belongs with the tool, so it travels across agents and keeps the system prompt focused. → **SYSTEM_PROMPT.md**\n- **Sub-workflow tools (`.toolWorkflow`) for anything multi-step.** Any workflow becomes a tool with typed `$fromAI()` inputs, and composes with branching, error handling, and reuse. Default here when in doubt. → **SUBWORKFLOW_AS_TOOL.md** and **n8n-subworkflows**.\n- **Wrap tools with user-visible side effects in human review.** Sends, payments, refunds, account changes get gated behind an approval node so a human signs off before the tool fires. → **HUMAN_REVIEW.md**\n- **Raise `maxIterations`.** The default tool-call cap is **low** (single digits on most versions) — fine for a one-tool agent, far too low for a multi-tool agent that chains several calls per turn. It surfaces as \"max iterations reached\" or empty output. Set `options.maxIterations` to a realistic ceiling (15 for a focused sub-agent, 50-200 for a broad orchestrator).\n- **Put the current date in the system prompt** via `{{ $now }}` (or `{{ $now.format('DDDD') }}`). A hardcoded date is stale immediately.\n\n---\n\n## The four tool types\n\nPick the lightest option that covers the job:\n\n| Tool type | Node | Use when |\n|---|---|---|\n| **Native tool node** | `slackTool`, `gmailTool`, `toolCalculator`, … | The capability maps to one existing node + one operation. Lowest overhead. |\n| **Sub-workflow as tool** | `.toolWorkflow` | More than one node, reusable logic, or you want independent testability. The canonical n8n way — **default when in doubt**. |\n| **HTTP Request Tool** | `.toolHttpRequest` | A single external HTTP API the agent should orchestrate directly. Reuse the service's predefined credential to cover operations a native node doesn't expose. |\n| **MCP Client Tool** | `.mcpClientTool` | A maintained MCP server already covers it, or you want one published workflow to serve many agents. |\n\nThere is also a **Custom Code Tool** (`.toolCode`) for pure inline computation — but its runtime contract (string in / string out, no `$fromAI`, no `$helpers`) is owned by the **n8n-code-tool** skill. Read that before writing one. Rule of thumb: if you find yourself reaching for `$fromAI()` inside the code, you want `.toolWorkflow` instead.\n\n### `$fromAI()`: how the agent fills tool parameters\n\nTool parameters the agent should decide are wrapped in `$fromAI()`. It is a **real n8n expression helper**, used inside a tool node's parameter expressions:\n\n```\n={{ $fromAI('paramName', 'what to put here — be specific: format, range, example', 'string') }}\n```\n\n- **paramName** — the name the model uses internally (snake_case or camelCase, be consistent).\n- **description** — tells the model what value to produce. **It is part of the prompt** — write it like JSDoc.\n- **type** (optional) — `'string'` (default), `'number'`, `'boolean'`, `'json'`. A wrong-typed value fails the call.\n- **defaultValue** (optional) — used when the model omits it.\n\n`$fromAI()` carries JSON only — it **cannot carry binary** (no base64, no file bytes). And not every parameter has to be `$fromAI`: plumb identity, authority limits, and correlation IDs (`userId`, refund caps, `sessionId`) deterministically from workflow context so the agent can't get them wrong or even see them. → **TOOLS.md** for the full anatomy and the \"give the agent a button, not a steering wheel\" pattern.\n\n---\n\n## System prompt vs tool description\n\n| Belongs in the **system prompt** | Belongs in the **tool's description** |\n|---|---|\n| Persona, role, voice | What this specific tool does |\n| Global output/format rules (\"respond in markdown\") | When to use it vs other tools |\n| Refusal / safety behavior | What each parameter means and its shape |\n| Display protocols (`![]()` for images) | Examples of good vs bad invocations |\n| Universal context (current date via `$now`, user role) | Tool-specific gotchas (rate limits, edge cases) |\n| Inter-tool flow (\"after generating, always display\") | Tool-specific input transformations |\n\nWhy split it: a well-described tool works in **any** agent that drops it in, tool details only \"load\" when the model considers that tool (token efficiency), and you update one tool description instead of a paragraph buried in a 5000-token prompt. → **SYSTEM_PROMPT.md**\n\n---\n\n## Structured output: when and how\n\nAdd an `outputParserStructured` sub-node (wired `ai_outputParser`) when downstream needs strict JSON, not free-form text. Two rules:\n\n1. **Use `schemaType: 'manual'` with a real JSON Schema, not `jsonSchemaExample`.** An example can't express required-vs-optional, enums, numeric ranges, or array constraints — you outgrow it the first time the shape gets non-trivial. Reach for `fromJson` + an example only for throwaway shapes.\n2. **`autoFix: true` with a coding-capable fixer model.** Wire a *second* model into the parser's `ai_languageModel` slot. Reconciling broken JSON against a schema is a coding task — a weak fixer just produces another malformed retry and burns tokens.\n\n→ **STRUCTURED_OUTPUT.md** for the schema patterns, the load-bearing \"DO NOT wrap in markdown\" retry line, and the parse-failure cookbook.\n\n---\n\n## Memory: brief mental model\n\nMemory is a sub-node (`ai_memory`). Without it, every call is stateless — correct for one-shot tasks (classify, summarize). With it, the agent holds a conversation, keyed by whatever expression you bind to `sessionKey`.\n\n- **`memoryBufferWindow`** — keeps the last N exchanges per key and persists across executions via n8n's store. The default for chat. **`contextWindowLength` defaults to 5, which is very low** — 50 is a saner starting point. Messages past the window are gone entirely.\n- **`memoryPostgresChat` / `memoryRedisChat`** — only when memory must be read *outside* the agent (your own UI, analytics, cross-system). Not needed just to survive restarts; BufferWindow already does that.\n\n**Plumb a stable key from the trigger to memory consistently.** Chat triggers fill `sessionId` automatically; for other surfaces derive one (Slack `thread_ts`, a webhook conversation ID). Never hardcode `sessionId: 'default'` and never put `sessionId` behind `$fromAI` (the model will fabricate a UUID). → **MEMORY.md**\n\n---\n\n## Binary and the agent boundary\n\nThis is the seam that trips people up:\n\n- **The model CAN see uploaded images** (vision) via `options.passthroughBinaryImages: true` on the agent.\n- **Tools CANNOT receive binary.** `$fromAI()` is JSON-only — no base64, no bytes, even through non-AI bindings.\n- **The agent's output is text-shaped** (or structured-text with a parser). When a model returns image/audio/video bytes, the Agent doesn't surface them at all — there's nothing to recover downstream.\n\n**Workaround:** pre-stage uploads to storage before the agent runs, inject the storage keys into the system prompt, and let tools accept the key as a string parameter and re-fetch internally. For one-shot media generation, skip the agent and call the provider's native single-call node directly.\n\nThe binary mechanics (which storage, how to stage, how to re-fetch) are owned by **n8n-binary-and-data** — see its agent-tool binary reference. This skill only marks the boundary; don't re-derive the mechanics here.\n\n---\n\n## Human review (gate destructive tools)\n\nWhen a tool's effect needs human sign-off before execution (sends, payments, refunds, account changes), wrap it with a review tool node — `slackHitlTool`, `discordHitlTool`, `telegramHitlTool`, `gmailHitlTool`, etc. (n8n names these \"Hitl\" / human-in-the-loop). The review node sits **between** the wrapped tool and the agent on the `ai_tool` connection: wrapped tool → review node → Agent.\n\nWhether sign-off is needed is a product/policy call — **surface the question to the user**, recommend based on blast radius, and let them decide.\n\n**The critical rule: show the actual parameters the wrapped tool will receive.** Use the literal `{{ $tool.parameters.<name> }}` in the approval message, never a `$fromAI()` paraphrase — otherwise the human approves text the model made up, not the call about to fire. → **HUMAN_REVIEW.md**\n\n---\n\n## Chat agents (Slack, Discord, Teams, Telegram)\n\n**The one non-negotiable, regardless of complexity:** any chat-triggered workflow that posts a reply MUST **filter out the bot's own user ID**, or its own replies re-trigger it in an infinite loop that burns runs and tokens. Prefer trigger-level filtering when available (Slack Trigger's `options.userIds` is an **exclusion list** — put the bot ID there); otherwise filter `$json.user !== '<BOT_USER_ID>'` in the first node after the trigger.\n\nBeyond the filter, a simple bot (trigger → agent → reply) lives fine in one workflow. Split into **shell + core + sub-agents** only once you need loading UX, sub-agents, multi-surface reuse, or robust error handling:\n\n- **Shell** — trigger, anti-loop filter, event-type Switch, loading/error UX, renders the reply. No LLM.\n- **Core** — stateless agent, `chatInput` + `threadId` inputs, memory keyed on `threadId`, tools and sub-agents.\n- **Sub-agents** — one narrow domain each, called via `.toolWorkflow`, **stateless** (full context in `chatInput`).\n\n→ **CHAT_AGENT_PATTERNS.md** for per-surface semantics, threading-as-session, and the full topology.\n\n---\n\n## Persisted n8n Agents (n8n_manage_agents)\n\nA **persisted n8n Agent** is a different artifact from the AI Agent node covered above: a standalone assistant record — model, instructions, tools, skills, tasks, memory, channels — stored and versioned by n8n itself, managed through `n8n_manage_agents` (n8n's instance-level MCP server), not a node inside a workflow's JSON.\n\n| You need to… | Use |\n|---|---|\n| A reasoning step inside a workflow, wired with `ai_*` sub-nodes | **AI Agent node** (this skill, above) |\n| A standalone assistant with its own lifecycle — draft, validate, publish, versions, channels — independent of any one workflow | **Persisted Agent** (`n8n_manage_agents`) |\n\n**Prerequisites:** `N8N_MCP_ACCESS_TOKEN` configured (separate from the Public API key) and n8n **2.34+** with the agents module enabled. The token is required for **every** action, including `reference`/`search` — without it, nothing works. Separately, `reference` and `search` work for **any** agent regardless of MCP exposure; every other action needs the target agent exposed to MCP (agents created through this tool are exposed automatically — the exposure gate only matters for agents that already existed before this tool touched them).\n\n**Build sequence:**\n1. `action: \"reference\"` — read the config schema and the exact mutate operations before anything else.\n2. `action: \"discover_assets\"` — list what the agent can actually be wired to. Takes `projectId` (from `n8n_list_catalog({kind: \"projects\"})`) and `kind`: `models` (with a `provider`), `integrations`, `workflows`, `subagents` or `mcpServers`. One call per kind.\n3. `action: \"create\"` — `projectId`, `name`, `config`.\n4. `action: \"mutate\"` — one resource per call (`config.patch`, `skill.upsert`/`delete`, `task.upsert`/`delete`, `customTool.upsert`/`delete`), always carrying the **latest** hash forward. Mind the two names: n8n returns it as `configHash` and expects it back as `args.baseConfigHash`. `args` are forwarded to n8n verbatim, so a near-miss on any field name comes back as `INVALID_ARGS`, not a helpful correction — which is why step 1 reads the schema first. A stale hash comes back as `STALE_CONFIG` — re-`get` and retry with the fresh one.\n5. `action: \"validate\"` — before offering to `call` or `publish`.\n6. `action: \"publish\"` — **only on the user's explicit request**, never proactively.\n\n`action: \"call\"` runs the agent with real credentials and real tools — a live execution, not a dry run. A result can carry `approvals[]` for tool calls that need a human decision; **never approve on the user's behalf** — surface them and resume only after the user decides.\n\n**Custom tools are a third code runtime — don't reuse either of the others.** A `customTool.upsert` body is **TypeScript**, and the only imports it may use are `@n8n/agents` and `zod`. This is not the Code node (JavaScript/Python, returns `[{json: …}]`) and not the AI-agent Custom Code Tool covered by **n8n-code-tool** (`@n8n/n8n-nodes-langchain.toolCode`, returns a string, no `$fromAI()`). Reaching for the wrong contract is the easy mistake here, because all three are \"write code the agent calls\". Read the shape from `action: \"reference\"` before writing one; a compile failure or an unknown `agentId` surfaces as `AGENT_TOOL_ERROR`.\n\n**Credential caveat:** on n8n 2.36.x the agents runtime rejects `azureOpenAiApi` and `aws` credentials (reported as `missing: [\"credential\"]`); the response's `hint` names the accepted types instead.\n\n**Testing without leaving debris:** name throwaway agents `[TEST] …` and `delete` them when you're done — a persisted Agent outlives the conversation that made it, unlike a workflow you can leave inactive.\n\n→ **n8n-mcp-tools-expert** `## Agents` for the tool's full action list and error codes.\n\n---\n\n## RAG (retrieval augmented generation)\n\nn8n ships the LangChain RAG primitives (document loaders, splitters, embeddings, vector stores, retrievers). Two opinions worth stating up front:\n\n1. **Rule out cheaper lookups first.** Exact lookups → a database or Data Table query, not RAG. Freshness → a live search tool. A small/structured doc set → give the agent list/fetch tools. Reach for a vector store only when there are too many docs to list and queries are semantic.\n2. **Wire the vector store as a retrieval tool** (`mode: 'retrieve-as-tool'`, `ai_tool`) so the agent decides when retrieval is relevant and can phrase the query itself. Embed query and documents with the **same** model.\n\n→ **RAG.md** (intentionally thin — defaults depend on data shape and scale).\n\n---\n\n## Reference files\n\n| File | Read when |\n|---|---|\n| **TOOLS.md** | Adding tools, choosing among the four types, writing names/descriptions, `$fromAI` anatomy |\n| **SUBWORKFLOW_AS_TOOL.md** | Wiring a sub-workflow as a tool via `.toolWorkflow`, mapping agent-filled vs plumbed params |\n| **SYSTEM_PROMPT.md** | Writing/refactoring a system prompt, the system-prompt-vs-tool-description split |\n| **STRUCTURED_OUTPUT.md** | Forcing JSON output, configuring autoFix, the fixer model, parse-failure fixes |\n| **MEMORY.md** | Choosing a memory type, persistence, sessionId handling |\n| **HUMAN_REVIEW.md** | Adding human approval, approval-message content, multi-channel approver |\n| **CHAT_AGENT_PATTERNS.md** | Building a Slack/Discord/Teams/Telegram bot, shell + core + sub-agents topology |\n| **RAG.md** | Retrieval-augmented agents (thin by design) |\n| **EXAMPLES.md** | Concrete node-object snippets: stateless agent core, Slack router shell, domain sub-agent |\n\n---\n\n## Anti-patterns\n\n| Anti-pattern | What goes wrong | Fix |\n|---|---|---|\n| Generic tool names (`tool1`, `doStuff`, `runQuery`) | Model can't tell which tool to pick — skips them or hallucinates params | Verb-first specific names: `Search customer database`, `Generate image with Veo` |\n| Empty or one-line tool descriptions | Model has no idea when to invoke; bad selection, no error | Write a real description: what it does, when to use, what each param means |\n| Cramming per-tool instructions into the system prompt | Bloated prompt, no reuse, per-tool guidance buried | Move tool-specific instructions into tool descriptions |\n| Agent + Switch to route on natural language | Two nodes + prompt boilerplate where Text Classifier is one node | Use Text Classifier — each category gets its own output handle (name **and** description) |\n| Wrapping image/audio/video generation in an Agent | Binary doesn't flow through tools or out of the agent output | Use the provider's native single-call node directly |\n| `outputParserStructured` without `autoFix` | One malformed response halts the workflow | `autoFix: true` + a coding-capable fixer model |\n| Passing binary directly to a tool | Doesn't work — binary can't cross the tool boundary | Pre-stage to storage, pass keys; see **n8n-binary-and-data** |\n| Hardcoded `sessionId` / no sessionId / `sessionId` behind `$fromAI` | Conversations cross, or the model fabricates a UUID | Plumb a stable key from the trigger to memory and tools |\n| Two near-identical tools | Selection is non-deterministic, model gets confused | One tool with internal branching driven by a parameter |\n| Chat bot with no bot-user filter | Its own replies re-trigger it → infinite loop | Exclude the bot user ID at the trigger or first node |\n| `maxIterations` left at the low default on a multi-tool agent | \"Max iterations reached\" / empty output | Raise `options.maxIterations` |\n| Filling the human-review message via `$fromAI()` | Approver signs off on a paraphrase, not the real call | Use literal `{{ $tool.parameters.<name> }}` |\n\n---\n\n## What's NOT available via the community MCP\n\n| Want to do | Reality |\n|---|---|\n| Chat-test a workflow's AI Agent node end-to-end interactively | `n8n_test_workflow` runs the workflow, but a true multi-turn chat session against the node is a UI activity (canvas chat tester). A persisted Agent, by contrast, can be run live via `n8n_manage_agents` `call` — see \"Persisted n8n Agents\" above. |\n| Set credentials' actual secret values | `n8n_manage_credentials` creates/updates credential records, but the agent provider keys themselves are entered/verified in the UI. |\n| Assign a workflow's Error Workflow | UI only — see **n8n-error-handling**. Build the catch-all, then hand the user the UI step. |\n| Pin the exact model availability per instance | Model lists shift between versions — `search_nodes`/`get_node` reflect what's installed. Verify on the target instance. |\n\nWhat the MCP **can** do: search and inspect every LangChain node (`search_nodes`, `get_node`), validate node config and the whole graph (`validate_node`, `validate_workflow`), build and patch the agent and its sub-nodes (`n8n_update_partial_workflow` with `addConnection` on `ai_*` outputs), test (`n8n_test_workflow`), and pull the saved JSON to verify wiring (`n8n_get_workflow`). The deep AI-agent guide also lives in `tools_documentation({topic: \"ai_agents_guide\", depth: \"full\"})`.\n\n---\n\n## Integration with other skills\n\n- **n8n-workflow-patterns** (`ai_agent_workflow.md`) — the high-level \"agent in a workflow\" shape. This skill is the deep dive; start there for architecture.\n- **n8n-mcp-tools-expert** — node-type formats (short form for `get_node`, long form in JSON) and tool-selection guidance. Consult before any MCP call.\n- **n8n-node-configuration** — `displayOptions`-driven fields on the agent and sub-nodes; Slack/Block Kit message shapes (`NODE_FAMILY_GOTCHAS.md`, Slack section).\n- **n8n-expression-syntax** — `{{ }}`, `$json.output`, `$now`, and `$fromAI`/`$tool.parameters` all rely on correct expression syntax.\n- **n8n-code-tool** — the Custom Code Tool's runtime contract (string in/out, no `$fromAI`). Read it before writing a `.toolCode`.\n- **n8n-subworkflows** — the sub-workflow primitive that `.toolWorkflow` builds on (Execute Workflow Trigger inputs/outputs, naming, search-before-build).\n- **n8n-binary-and-data** — owns the agent-tool binary boundary mechanics (staging uploads, returning generated files).\n- **n8n-validation-expert** — interpreting `validate_workflow` results, including AI-connection issues (a tool wired into `main` instead of `ai_tool` flags as disconnected).\n- **n8n-error-handling** — `onError: 'continueErrorOutput'` on tool sub-workflows and the agent-core call; error UX on chat shells.\n- **n8n-code-javascript / n8n-code-python** — for Code-node logic *inside* a tool sub-workflow (different sandbox from the Code Tool).\n\n---\n\n## Quick reference checklist\n\nBefore shipping an agent:\n\n- [ ] **Right node**: Agent for tools/memory/multi-turn; Text Classifier for routing; Information Extractor for fields; native node for media\n- [ ] **Model** wired via `ai_languageModel`\n- [ ] **Every tool** has a verb-first specific name AND a real description\n- [ ] **`$fromAI()` descriptions** are specific (format, range, example); identity/limits/sessionId plumbed deterministically, not via `$fromAI`\n- [ ] **Per-tool guidance** lives in tool descriptions, not the system prompt\n- [ ] **`$now`** in the system prompt (no hardcoded date)\n- [ ] **`maxIterations`** raised for multi-tool agents\n- [ ] **Memory** keyed on a stable `sessionKey` from the trigger (not `'default'`, not `$fromAI`); `contextWindowLength` raised from 5\n- [ ] **Structured output**: `schemaType: 'manual'` + `autoFix: true` + a coding-capable fixer model\n- [ ] **Destructive tools** wrapped in human review; approval message uses `$tool.parameters`, not `$fromAI`\n- [ ] **Chat bots** filter the bot's own user ID (trigger-level or first node)\n- [ ] **Binary**: model vision via `passthroughBinaryImages`; tools get storage keys, never bytes\n- [ ] **Validated** with `validate_workflow` and verified with `n8n_get_workflow` (sub-nodes on `ai_*`, not `main`)\n\n---\n\n**Remember**: an agent is only as good as its tool names, descriptions, and system-prompt discipline. The model can't see your wiring — it sees a system prompt and a list of named, described tools. Design those like an API and most \"the agent won't behave\" problems disappear.\n\n<!-- File: data/skills/n8n-agents/STRUCTURED_OUTPUT.md (Tokens: ~300 | Category: anthropic-skill) -->\n# N8n-agents Skill\n\nPath: `data/skills/n8n-agents/STRUCTURED_OUTPUT.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-agents/STRUCTURED_OUTPUT.md)\n\n<!-- File: data/skills/n8n-agents/SUBWORKFLOW_AS_TOOL.md (Tokens: ~300 | Category: anthropic-skill) -->\n# N8n-agents Skill\n\nPath: `data/skills/n8n-agents/SUBWORKFLOW_AS_TOOL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-agents/SUBWORKFLOW_AS_TOOL.md)\n\n<!-- File: data/skills/n8n-agents/SYSTEM_PROMPT.md (Tokens: ~300 | Category: anthropic-skill) -->\n# N8n-agents Skill\n\nPath: `data/skills/n8n-agents/SYSTEM_PROMPT.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-agents/SYSTEM_PROMPT.md)\n\n<!-- File: data/skills/n8n-agents/TOOLS.md (Tokens: ~300 | Category: anthropic-skill) -->\n# N8n-agents Skill\n\nPath: `data/skills/n8n-agents/TOOLS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-agents/TOOLS.md)\n\n<!-- File: data/skills/n8n-binary-and-data/AGENT_TOOL_BINARY.md (Tokens: ~300 | Category: anthropic-skill) -->\n# N8n-binary-and-data Skill\n\nPath: `data/skills/n8n-binary-and-data/AGENT_TOOL_BINARY.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-binary-and-data/AGENT_TOOL_BINARY.md)\n\n<!-- File: data/skills/n8n-binary-and-data/BINARY_BASICS.md (Tokens: ~300 | Category: anthropic-skill) -->\n# N8n-binary-and-data Skill\n\nPath: `data/skills/n8n-binary-and-data/BINARY_BASICS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-binary-and-data/BINARY_BASICS.md)\n\n<!-- File: data/skills/n8n-binary-and-data/CDN_REQUIREMENT.md (Tokens: ~300 | Category: anthropic-skill) -->\n# N8n-binary-and-data Skill\n\nPath: `data/skills/n8n-binary-and-data/CDN_REQUIREMENT.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-binary-and-data/CDN_REQUIREMENT.md)\n\n<!-- File: data/skills/n8n-binary-and-data/MERGE_FOR_CONTEXT.md (Tokens: ~300 | Category: anthropic-skill) -->\n# N8n-binary-and-data Skill\n\nPath: `data/skills/n8n-binary-and-data/MERGE_FOR_CONTEXT.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-binary-and-data/MERGE_FOR_CONTEXT.md)\n\n<!-- File: data/skills/n8n-binary-and-data/README.md (Tokens: ~300 | Category: anthropic-skill) -->\n# N8n-binary-and-data Skill\n\nPath: `data/skills/n8n-binary-and-data/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-binary-and-data/README.md)\n\n<!-- File: data/skills/n8n-binary-and-data/SKILL.md (Tokens: ~3699 | Category: anthropic-skill) -->\n---\nname: n8n-binary-and-data\ndescription: Handle files and binary data in n8n correctly. Use when working with files, images, PDFs, attachments, uploads or downloads, base64, vision/multimodal input, or when an AI agent needs a file as tool input or output — and whenever the user mentions $binary, binaryPropertyName, \"read the PDF\", \"attach the file\", \"send the image\", Merge losing binary, or a CDN for chat images. Covers the $binary vs $json split, reading/writing binary, keeping binary alive across transforms with Merge, the agent-tool binary boundary, and the CDN/URL requirement for chat surfaces.\n---\n\n# n8n Binary and Data\n\nEvery n8n item carries two independent slots: `$json` for structured data and `$binary` for file bytes. They travel side by side through the workflow. File contents — the actual PDF, image, or zip — live in `$binary`, never in `$json`. Get that split wrong and you read an empty field, lose a file mid-flow, or hand an AI agent a tool input it can't use.\n\nThis skill covers where binary lives, how to read and write it, how to keep it from being silently stripped, the hard wall between binary and the AI-agent tool boundary, and why chat surfaces need a URL instead of raw bytes.\n\n---\n\n## The three rules that prevent 90% of binary bugs\n\n1. **File contents are in `$binary`, not `$json`.** After an HTTP download, a \"Read Files\", or an email-attachment trigger, the bytes sit in `$binary.<key>`. `$json` holds metadata at most. Reading `$json.data` for file contents gives you nothing.\n\n2. **Binary cannot cross the AI-agent tool boundary — in either direction.** Tool arguments and tool return values are JSON only. An uploaded image can't be passed into a tool as a file, and a tool can't return raw bytes. Pre-stage to storage and pass a key or URL through JSON instead. See `AGENT_TOOL_BINARY.md`.\n\n3. **Chat surfaces render images by URL, not by `$binary`.** Slack, Discord, Teams, Telegram, embedded webhook chat — none of them read the binary slot. The image has to live somewhere a URL can fetch it. See `CDN_REQUIREMENT.md`.\n\n---\n\n## The two slots\n\nEach item is shaped like this:\n\n```json\n{\n  \"json\": { \"customerId\": 42, \"status\": \"sent\" },\n  \"binary\": {\n    \"invoice\": {\n      \"data\": \"<base64-encoded bytes>\",\n      \"mimeType\": \"application/pdf\",\n      \"fileName\": \"invoice-42.pdf\",\n      \"fileExtension\": \"pdf\"\n    }\n  }\n}\n```\n\nThe key inside `binary` (`invoice` here) is the **binary property name**. Most file-handling nodes have a `binaryPropertyName` parameter that points at it — the producer names the slot, the consumer references it by that name. The default key across most nodes is `data`, so when nothing tells you otherwise, assume `$binary.data`.\n\n`$json` and `$binary` are separate namespaces. An expression like `{{ $binary.invoice.fileName }}` reads file metadata; `{{ $json.customerId }}` reads data. They never mix.\n\nThis split also explains a webhook gotcha: a Webhook trigger receiving `multipart/form-data` puts the uploaded file in `$binary` and the accompanying form fields in `$json.body` — so an uploaded file is not somewhere under `$json` at all. (The `$json.body` nesting for webhooks is **n8n-expression-syntax** territory.)\n\nSee `BINARY_BASICS.md` for the full slot anatomy, mime types, and size limits.\n\n---\n\n## Producing binary\n\nYou rarely build a `$binary` slot by hand — nodes populate it for you:\n\n| Source | How binary appears |\n|---|---|\n| HTTP Request with `responseFormat: \"file\"` | Response body lands in `$binary.data` (or the name you set) |\n| Read/Write Files from Disk | File contents read into `$binary` |\n| Storage downloads (S3, Google Drive, Dropbox, etc.) | Downloaded file in `$binary.<key>` |\n| Email triggers with attachments | Each attachment arrives in `$binary` |\n| Provider AI media nodes (image/audio gen) | Set `options.binaryPropertyOutput` so the bytes land where the next node looks |\n\nFor an HTTP download, the one field that matters is `responseFormat`. Confirm it with `get_node` on `nodes-base.httpRequest` — leaving it as the default JSON/string format is the classic reason a downloaded file ends up as garbled text in `$json` instead of clean bytes in `$binary`.\n\n---\n\n## Reading and writing binary in a Code node\n\nMost workflows never need to crack open the bytes — they just pass binary through to a consumer (email attachment, file upload, Slack file). When you do need the raw bytes, do it in a Code node.\n\n**Read** with `getBinaryDataBuffer` — do not try to base64-decode `$binary.<key>.data` by hand:\n\n```javascript\n// Code node, \"Run Once for Each Item\"\nconst buffer = await this.helpers.getBinaryDataBuffer(0, 'data'); // (itemIndex, propertyName)\nconst text = buffer.toString('utf-8');\nconst length = buffer.length;\n\nreturn [{\n  json: { ...$json, length },\n  binary: $input.item.binary,   // pass the binary through, or it's gone\n}];\n```\n\n**Write** by building the slot yourself — base64 the bytes plus a mime type and file name:\n\n```javascript\nconst text = 'Hello, world!';\nreturn [{\n  json: { ok: true },\n  binary: {\n    report: {\n      data: Buffer.from(text).toString('base64'),\n      mimeType: 'text/plain',\n      fileName: 'report.txt',\n      fileExtension: 'txt',\n    },\n  },\n}];\n```\n\nThe Code-node sandbox, helpers, and execution modes are the domain of **n8n-code-javascript** (and **n8n-code-python**) — use those for the language-level detail. The one binary-specific thing to remember here: a Code node that returns `[{ json: {...} }]` without re-attaching `binary` **silently drops the file**. See `BINARY_BASICS.md`.\n\n---\n\n## Keeping binary alive across transforms\n\nJSON-only nodes — Edit Fields (Set), Code, IF, and others — can drop the `$binary` slot from their output. The workflow validates clean and runs without error; the file just isn't there downstream when the email node goes to attach it.\n\nTwo ways to keep it:\n\n- **Pass-through option on the transforming node.** Edit Fields has `includeOtherFields`; a Code node can return `binary: $input.item.binary` explicitly. Cheapest fix when it's available.\n- **Fan out and Merge by position.** Route the source into both the transform and a bypass branch, then recombine with a Merge in `combineByPosition` mode. The JSON comes from the transform side, the binary survives on the bypass side.\n\n```\n[Source with binary] ─┬─→ [Edit Fields: change JSON] ─┐\n                      │      (binary stripped here)     ├─→ [Merge: combineByPosition] ─→ [Email: attach]\n                      └──────────────────────────────────┘\n                          (bypass — binary passes through untouched)\n```\n\n`combineByPosition` pairs item N from each input, so the field counts must line up. The connection wiring and the alternatives for many-strip-point chains (upload-early, sub-workflow) are in `MERGE_FOR_CONTEXT.md`.\n\n---\n\n## The agent-tool binary boundary\n\nThis is the sharpest edge. An AI Agent talks to its tools (Custom Code Tool, Call n8n Workflow Tool, HTTP Request Tool, MCP tools) over JSON. Binary does not fit through that pipe in either direction. The fix is the same shape both ways: **stage the bytes in storage, pass a key/URL through JSON, fetch on the other side.**\n\n**Inbound — a user uploads a file the agent's tool must operate on:**\n\n1. The chat trigger gives you a `files[]` array. Split it out and upload each file to private storage under a hashed key.\n2. Re-merge that branch before the agent runs (it's a synchronization barrier, not decoration), and set `executeOnce: true` on the agent so N files don't trigger N agent runs.\n3. Inject the keys into the agent's system prompt, listing both the original name (human context) and the storage key (what the tool needs), with an explicit \"use EXACTLY this key\".\n4. The tool receives the key as a string argument and downloads the file from storage itself.\n\n**Outbound — a tool generates a file the agent must return:**\n\n1. The tool sub-workflow generates the binary, uploads it to storage, and returns JSON like `{ \"ok\": true, \"key\": \"...\", \"url\": \"https://...\", \"mimeType\": \"image/png\" }`.\n2. The agent embeds the URL in its reply (or passes the key to another tool).\n\n`passthroughBinaryImages: true` on the agent only changes what the **LLM sees** for vision — it does **not** let tools receive the file, and it's image-only (no PDFs, audio, or video). You still need the upload-and-pass-key pattern for any tool. Full patterns, hash strategy, storage choices, and the long-running-tool variant are in `AGENT_TOOL_BINARY.md`.\n\n> Building the tool itself? See **n8n-code-tool** for the Custom Code Tool contract and **n8n-workflow-patterns** for the AI-Agent-with-tools shape.\n\n---\n\n## The CDN requirement for chat surfaces\n\nWhen a workflow generates an image and the user wants it shown inside a chat message:\n\n- **Binary on the item isn't enough.** The chat client renders messages that reference images by URL (or pushes bytes through the platform's own file-upload API). It never reads `$binary`.\n- **The bytes have to live somewhere a URL can fetch over HTTPS.** Upload to an object store or drive first, then embed the returned URL.\n- **n8n has no built-in CDN.** The user provides the storage.\n\nAsk which storage they already use rather than defaulting to S3 — object storage (S3, R2, GCS, Azure Blob, Backblaze B2, Supabase Storage) and drive-style services (Dropbox, Google Drive, OneDrive, Box) all work and all change the URL shape. Cloudflare R2 is the lowest-friction starting point if they have nothing. For sensitive content, use a signed URL with an expiry rather than a permanently public one. See `CDN_REQUIREMENT.md`.\n\n---\n\n## What's NOT available\n\n- **`$fromAI()` cannot carry binary.** It fills tool parameters with strings, numbers, booleans, and objects — never file bytes. Pass a storage key instead.\n- **Tool arguments and returns are JSON only.** There is no \"binary parameter\" on an agent tool, in or out.\n- **n8n ships no CDN or public file host.** Serving a file over a URL is always something the user's storage does, not n8n.\n- **`getBinaryDataBuffer` is a Code-node helper.** It isn't available in the Custom Code Tool sandbox (see **n8n-code-tool**).\n\n---\n\n## Where Data Tables live\n\nFor persistent tabular storage — reference-counting staged files, tracking which keys are live, dedup — that's the `n8n_manage_datatable` surface, owned by **n8n-mcp-tools-expert**. This skill does not cover Data Tables.\n\n---\n\n## Anti-patterns\n\n| Anti-pattern | What goes wrong | Fix |\n|---|---|---|\n| Reading file contents from `$json` | Bytes live in `$binary`; `$json` is empty or metadata only | Read `$binary.<key>`, or `getBinaryDataBuffer` in a Code node |\n| HTTP download without `responseFormat: \"file\"` | Bytes arrive as mangled text in `$json`, not clean binary | Set `responseFormat: \"file\"` on the HTTP Request node |\n| Code node returns `[{json:{...}}]`, no `binary` | The file is silently dropped downstream | Re-attach `binary: $input.item.binary` in the return |\n| JSON transform (Edit Fields/IF) eats the binary | Email/upload node finds nothing to attach | Pass-through option, or fan out + Merge by position |\n| Passing an uploaded file into a tool via `$fromAI` | `$fromAI` can't carry binary; the tool gets nothing | Pre-stage to storage, inject the key in the system prompt, tool fetches by key |\n| Assuming `passthroughBinaryImages` lets tools see the file | It only affects what the LLM sees, and only for images | Still need the upload-and-pass-key pattern for tools |\n| Tool returns raw binary to the agent | Tool output is JSON; bytes don't survive (and bloat context) | Upload, return `{ key, url }` in JSON |\n| Posting `$binary` to a chat surface and expecting an image | Chat clients render by URL, not raw bytes | Upload to storage/CDN, embed the URL or use the platform file API |\n| Hardcoding base64 in a Code node | Huge workflow JSON, slow, leaky | Reference via `$binary`, or upload and reference by URL |\n\n---\n\n## Reference files\n\n| File | Read when |\n|---|---|\n| `BINARY_BASICS.md` | First time handling binary, or reading/writing the `$binary` slot, mime types, size limits |\n| `AGENT_TOOL_BINARY.md` | An agent tool needs an uploaded file, or produces one — the boundary in either direction |\n| `MERGE_FOR_CONTEXT.md` | Binary disappears after a JSON transform and you need to re-attach it |\n| `CDN_REQUIREMENT.md` | Showing images in a chat surface or anywhere that needs URL-referenced images |\n\n---\n\n## Integration with Other Skills\n\n**n8n-code-javascript / n8n-code-python**: the Code node is where you read/write raw bytes (`getBinaryDataBuffer`, `Buffer.from(...).toString('base64')`). Those skills own the sandbox, helpers, and execution-mode detail — this skill owns the rule that binary must be re-attached on return.\n\n**n8n-code-tool**: the Custom Code Tool sandbox is narrower — no `$binary`, no `getBinaryDataBuffer`, no `$fromAI`. When a tool needs a file, this skill's storage-key pattern is how it gets one.\n\n**n8n-workflow-patterns**: the agent-tool binary boundary sits inside the AI-Agent-with-tools pattern; the CDN flow is a generate → upload → reply chain.\n\n**n8n-node-configuration**: `responseFormat`, `binaryPropertyName`, `includeOtherFields`, `binaryPropertyOutput` are all conditional fields — use `get_node` to confirm the exact names on the user's version.\n\n**n8n-expression-syntax**: addressing `$binary.<key>.fileName` vs `$json.body` (webhook uploads in particular) is expression territory.\n\n**n8n-validation-expert**: a dropped binary slot is a silent failure — `validate_workflow` won't flag it. Confirm presence by inspecting the execution.\n\n**n8n-mcp-tools-expert**: owns `n8n_manage_datatable` (Data Tables) and `n8n_executions` — use the latter to confirm a `binary` slot actually survived a given node.\n\n**n8n-error-handling**: storage uploads and downloads fail; the inbound/outbound staging steps need error branches so a missing key doesn't 404 silently.\n\n**using-n8n-mcp-skills**: the index of how these skills fit together.\n\n---\n\n## Verifying binary survived\n\nValidation won't catch a stripped binary slot — it's a silent failure. Confirm it ran correctly:\n\n1. `n8n_test_workflow` (or trigger a real run) to produce an execution.\n2. `n8n_executions` to pull that execution, and inspect per-node output for the `binary` slot — it shows presence and metadata even if the base64 is too large to render.\n3. The node where `binary` last appears is the node before the strip. That's where the pass-through or Merge goes.\n\n---\n\n## Quick Reference Checklist\n\n- [ ] File contents read from `$binary.<key>` — never `$json`\n- [ ] HTTP downloads use `responseFormat: \"file\"`\n- [ ] Code nodes re-attach `binary` on return when the file must continue\n- [ ] JSON transforms either pass binary through or Merge it back (`combineByPosition`)\n- [ ] No attempt to pass binary into/out of an agent tool — keys/URLs through JSON instead\n- [ ] `passthroughBinaryImages` used only for LLM vision, not as a tool channel\n- [ ] Chat-surface images uploaded to storage; the URL is embedded, not the bytes\n- [ ] Storage backend chosen with the user (not defaulted to S3); signed URLs for sensitive content\n- [ ] Binary presence confirmed by inspecting the execution, not by validation\n\n---\n\n**Remember**: two slots, side by side. Data rides in `$json`, files ride in `$binary` — and the moment a file has to cross an agent tool or reach a chat surface, it travels as a URL, not as bytes.\n\n<!-- File: data/skills/n8n-code-javascript/BUILTIN_FUNCTIONS.md (Tokens: ~300 | Category: anthropic-skill) -->\n# N8n-code-javascript Skill\n\nPath: `data/skills/n8n-code-javascript/BUILTIN_FUNCTIONS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-code-javascript/BUILTIN_FUNCTIONS.md)\n\n<!-- File: data/skills/n8n-code-javascript/COMMON_PATTERNS.md (Tokens: ~300 | Category: anthropic-skill) -->\n# N8n-code-javascript Skill\n\nPath: `data/skills/n8n-code-javascript/COMMON_PATTERNS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-code-javascript/COMMON_PATTERNS.md)\n\n<!-- File: data/skills/n8n-code-javascript/DATA_ACCESS.md (Tokens: ~300 | Category: anthropic-skill) -->\n# N8n-code-javascript Skill\n\nPath: `data/skills/n8n-code-javascript/DATA_ACCESS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-code-javascript/DATA_ACCESS.md)\n\n<!-- File: data/skills/n8n-code-javascript/ERROR_PATTERNS.md (Tokens: ~300 | Category: anthropic-skill) -->\n# N8n-code-javascript Skill\n\nPath: `data/skills/n8n-code-javascript/ERROR_PATTERNS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-code-javascript/ERROR_PATTERNS.md)\n\n<!-- File: data/skills/n8n-code-javascript/README.md (Tokens: ~300 | Category: anthropic-skill) -->\n# N8n-code-javascript Skill\n\nPath: `data/skills/n8n-code-javascript/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-code-javascript/README.md)\n\n<!-- File: data/skills/n8n-code-javascript/SKILL.md (Tokens: ~4821 | Category: anthropic-skill) -->\n---\nname: n8n-code-javascript\ndescription: Write JavaScript code in n8n Code nodes. Use when writing JavaScript in n8n, using $input/$json/$node syntax, making HTTP requests with this.helpers / the $helpers global, working with dates using DateTime, troubleshooting Code node errors, choosing between Code node modes, or doing any custom data transformation in n8n. Always use this skill when a workflow needs a Code node — whether for data aggregation, filtering, API calls, format conversion, batch processing logic, or any custom JavaScript. Covers SplitInBatches loop patterns, cross-iteration data, pairedItem, and real-world production patterns. Also use when asked why a Code node or workflow is slow, which execution mode is faster, or how to cut per-item overhead on large datasets. EXCEPTION — for the AI-agent-callable Custom Code Tool (@n8n/n8n-nodes-langchain.toolCode, a tool attached to an AI Agent), use the n8n-code-tool skill instead; it has a different runtime contract.\n---\n\n# JavaScript Code Node\n\nExpert guidance for writing JavaScript code in n8n Code nodes.\n\n---\n\n## Quick Start\n\n```javascript\n// Basic template for Code nodes\nconst items = $input.all();\n\n// Process data\nconst processed = items.map(item => ({\n  json: {\n    ...item.json,\n    processed: true,\n    timestamp: new Date().toISOString()\n  }\n}));\n\nreturn processed;\n```\n\n### Essential Rules\n\n1. **Choose \"Run Once for All Items\" mode** (recommended for most use cases)\n2. **Access data**: `$input.all()`, `$input.first()`, or `$input.item`\n3. **Return `[{json: {...}}]`** — the canonical, mode-portable form. In *Run Once for All Items* mode n8n also auto-wraps a bare `return {…}` object, so that runs too; what genuinely fails is returning a primitive (string/number) or `null`.\n4. **CRITICAL**: Webhook data is under `$json.body` (not `$json` directly)\n5. **Built-ins available**: `this.helpers.httpRequest()` (no auth — the bare `$helpers` global is **undefined** in the task-runner sandbox, so `$helpers.httpRequest()` throws `ReferenceError: $helpers is not defined`), DateTime (Luxon), $jmespath(). **Not available**: `this.helpers.httpRequestWithAuthentication` (deny-listed), $env (when N8N_BLOCK_ENV_ACCESS_IN_NODE=true), require() (unless allowlisted). For anything beyond a trivial unauthenticated GET (auth, pagination, retries), prefer the **HTTP Request node** and keep Code nodes for pure logic.\n6. **Instance-allowlisted libraries**: Self-hosted instances can allowlist modules via `N8N_RUNNERS_ALLOWED_BUILT_IN_MODULES` and `N8N_RUNNERS_ALLOWED_EXTERNAL_MODULES` (legacy: `NODE_FUNCTION_ALLOW_BUILTIN` / `NODE_FUNCTION_ALLOW_EXTERNAL`). If the user says their instance allows specific modules (e.g. `axios`, `lodash`, `crypto`), use them via `require()` — don't refuse. If unsure, ask or default to built-ins only.\n7. **Wrong skill?** If you're writing code for a **Custom Code Tool** attached to an AI Agent (`@n8n/n8n-nodes-langchain.toolCode`), stop — that node has a different contract (input via `query`, must return a string, no `$input`/`$helpers`). Use the **n8n-code-tool** skill.\n\n---\n\n## Mode Selection Guide\n\nThe Code node offers two execution modes. Choose based on your use case:\n\n### Run Once for All Items (Recommended - Default)\n\n**Use this mode for:** 95% of use cases\n\n- **How it works**: Code executes **once** regardless of input count\n- **Data access**: `$input.all()` or `items` array\n- **Best for**: Aggregation, filtering, batch processing, transformations, API calls with all data\n- **Performance**: Faster for multiple items (single execution)\n\n```javascript\n// Example: Calculate total from all items\nconst allItems = $input.all();\nconst total = allItems.reduce((sum, item) => sum + (item.json.amount || 0), 0);\n\nreturn [{\n  json: {\n    total,\n    count: allItems.length,\n    average: total / allItems.length\n  }\n}];\n```\n\n**When to use:**\n- ✅ Comparing items across the dataset\n- ✅ Calculating totals, averages, or statistics\n- ✅ Sorting or ranking items\n- ✅ Deduplication\n- ✅ Building aggregated reports\n- ✅ Combining data from multiple items\n\n### Run Once for Each Item\n\n**Use this mode for:** Specialized cases only\n\n- **How it works**: Code executes **separately** for each input item\n- **Data access**: `$input.item` or `$item`\n- **Best for**: Item-specific logic, independent operations, per-item validation\n- **Performance**: Slower for large datasets (multiple executions)\n\n```javascript\n// Example: Add processing timestamp to each item\nconst item = $input.item;\n\nreturn [{\n  json: {\n    ...item.json,\n    processed: true,\n    processedAt: new Date().toISOString()\n  }\n}];\n```\n\n**When to use:**\n- ✅ Each item needs independent API call\n- ✅ Per-item validation with different error handling\n- ✅ Item-specific transformations based on item properties\n- ✅ When items must be processed separately for business logic\n\n**Decision Shortcut:**\n- **Need to look at multiple items?** → Use \"All Items\" mode\n- **Each item completely independent?** → Use \"Each Item\" mode\n- **Not sure?** → Use \"All Items\" mode (you can always loop inside)\n\n### Why \"All Items\" is faster — the per-item boundary\n\nMode choice is the single biggest performance lever in a Code node. Each *per-item* execution context costs a setup tax (measured on n8n 2.x, small records):\n\n| What runs per item | Approx. cost |\n|---|---|\n| Code **All Items** (one run for the whole set) | ~0.02 ms/item |\n| Expression in any node (IF / Set / etc.) | ~0.2 ms/item |\n| Code **Each Item** (a full sandbox per item) | ~0.6 ms/item — ~25–30× All Items |\n\nSo `Run Once for Each Item` over 10k items is ~6 s of pure overhead vs ~0.2 s in `Run Once for All Items`. Use Each Item only when an item genuinely needs isolating (independent error handling, or a per-item API call you can't batch); otherwise loop *inside* one All Items node. Expression complexity itself is essentially free (~90% of the cost is the per-item context, not your code) and every node→node hop re-copies all items — so reduce the *number* of per-item boundaries, don't micro-optimize each one. Below a few hundred items none of this matters; reach for it on the hot path (large item counts, little I/O).\n\n**See**: [DATA_ACCESS.md](DATA_ACCESS.md) → \"Mode Performance\" for the corollaries, hop costs, and scale check.\n\n---\n\n## Data Access Patterns\n\nFour ways to pull data from upstream nodes. Note `$node[\"Name\"]` and `$('Name')` need `.first().json` or `.all()` — never `.json` directly.\n\n```javascript\nconst allItems = $input.all();          // 1. All items — batch ops, aggregation (most common)\nconst data = $input.first().json;       // 2. First item — single objects, API responses\nconst item = $input.item;               // 3. Current item — \"Each Item\" mode ONLY (undefined otherwise)\nconst other = $node[\"Webhook\"].json;    // 4. Named node — combine data across nodes\n```\n\nAlways access fields via `.json` (e.g. `item.json.name`, not `item.name`), and prefer the explicit `$input.first().json.field` over a bare `$json.field`.\n\n**See**: [DATA_ACCESS.md](DATA_ACCESS.md) for the full guide — every pattern with examples, a decision tree, and the common mistakes (mutating originals, missing length checks, `$input.item` in the wrong mode).\n\n---\n\n## Critical: Webhook Data Structure\n\n**MOST COMMON MISTAKE**: Webhook data is nested under `.body`\n\n```javascript\n// ❌ WRONG - Will return undefined\nconst name = $json.name;\nconst email = $json.email;\n\n// ✅ CORRECT - Webhook data is under .body\nconst name = $json.body.name;\nconst email = $json.body.email;\n\n// Or with $input\nconst webhookData = $input.first().json.body;\nconst name = webhookData.name;\n```\n\n**Why**: Webhook node wraps all request data under `body` property. This includes POST data, query parameters, and JSON payloads.\n\n**See**: [DATA_ACCESS.md](DATA_ACCESS.md) for full webhook structure details\n\n---\n\n## Return Format Requirements\n\n**Canonical form**: `[{json: {...}}]` — an array of objects each with a `json` property. It is unambiguous and works identically in both execution modes, so make it your default.\n\nIn *Run Once for All Items* mode n8n auto-normalizes looser shapes on the way out: a single bare object, or an array of bare objects, gets wrapped under `json` for you. So `return {foo: 1}` runs. What has nothing to wrap — and therefore genuinely fails at runtime with \"Code doesn't return items properly\" — is a primitive (string/number/boolean) or `null`/`undefined`. (n8n-mcp ≥ 2.63.0 no longer flags a bare-object return as an error; it reflects this auto-wrap behavior.)\n\n### Correct Return Formats\n\n```javascript\n// ✅ Single result\nreturn [{\n  json: {\n    field1: value1,\n    field2: value2\n  }\n}];\n\n// ✅ Multiple results\nreturn [\n  {json: {id: 1, data: 'first'}},\n  {json: {id: 2, data: 'second'}}\n];\n\n// ✅ Transformed array\nconst transformed = $input.all()\n  .filter(item => item.json.valid)\n  .map(item => ({\n    json: {\n      id: item.json.id,\n      processed: true\n    }\n  }));\nreturn transformed;\n\n// ✅ Empty result (when no data to return)\nreturn [];\n\n// ✅ Conditional return\nif (shouldProcess) {\n  return [{json: processedData}];\n} else {\n  return [];\n}\n```\n\n### Non-Canonical Returns (auto-wrapped — prefer the canonical form)\n\n```javascript\n// ⚠️ Auto-wrapped in All Items mode → [{json: {field: value}}]. Runs, but prefer the array form.\nreturn {\n  json: {field: value}\n};\n\n// ⚠️ Auto-wrapped → [{json: {field: value}}]. Runs, but add the json wrapper for clarity.\nreturn [{field: value}];\n\n// ✅ Fine — input items already carry a json property, so returning them unchanged is a valid passthrough\nreturn $input.all();\n```\n\n### Genuinely Broken Returns\n\n```javascript\n// ❌ FAILS: primitive — n8n errors \"Code doesn't return items properly\"\nreturn \"processed\";\n\n// ❌ FAILS: null / undefined — nothing to pass to the next node\nreturn null;\n```\n\n**Why it matters**: The canonical `[{json: {...}}]` is unambiguous and behaves the same in both modes. n8n auto-normalizes bare objects and arrays-of-objects in All Items mode, but a primitive or `null` return has nothing to wrap and stops execution.\n\n**See**: [ERROR_PATTERNS.md](ERROR_PATTERNS.md) #3 for detailed error solutions\n\n---\n\n## Common Patterns Overview\n\nThe most useful Code node shapes from production workflows. One quick example — sum/aggregate across all items:\n\n```javascript\nconst items = $input.all();\nconst total = items.reduce((sum, item) => sum + (item.json.amount || 0), 0);\nreturn [{ json: { total, count: items.length, average: total / items.length } }];\n```\n\nThe full library covers 10 patterns: multi-source aggregation, regex filtering, markdown/structured-text parsing, JSON comparison, CRM/form transformation, release processing, array transformation with computed fields, Slack Block Kit formatting, top-N ranking, and string-aggregation reporting — each with variations.\n\n**See**: [COMMON_PATTERNS.md](COMMON_PATTERNS.md) for the 10 detailed production patterns (and the Best Practices section: validate input, try-catch, filter-early, array methods over loops, console.log debugging).\n\n---\n\n## Error Prevention - Top Mistakes\n\nThe recurring Code node failures, in rough frequency order:\n\n1. **Empty code / missing return** — always end with `return [...]`, and make sure *every* branch returns.\n2. **Expression syntax as code** — don't write `{{ }}` where JavaScript belongs (`return {{ $json.x }}` is a syntax error). Use `` `${$json.field}` `` or `$input.first().json.field`. `{{ }}` *inside a string literal* is fine — it's just literal text n8n won't evaluate.\n3. **Return shape** — prefer `return [{json:{...}}]`. A bare `return {…}` auto-wraps in All Items mode, but returning a primitive (string/number) or `null` is what actually fails.\n4. **Missing null checks** — use optional chaining: `item.json?.user?.email || 'fallback'`.\n5. **Webhook body nesting** — `$json.email` is undefined; use `$json.body.email`.\n6. **Auth helpers blocked** (`httpRequestWithAuthentication`) and `$env` blocked — route secrets through credentials/HTTP Request node, not the Code node sandbox.\n\n**See**: [ERROR_PATTERNS.md](ERROR_PATTERNS.md) for the comprehensive guide — each error with wrong/right code, escaping rules, the sandbox restrictions (Errors #6–#7), a prevention checklist, and a quick error-message lookup table.\n\n---\n\n## Built-in Functions & Helpers\n\n```javascript\n// HTTP requests (no auth — see sandbox note below)\nconst res = await this.helpers.httpRequest({ method: 'GET', url: 'https://api.example.com/data' });\n\n// DateTime (Luxon): now, formatting, arithmetic\nconst now = DateTime.now();\nconst formatted = now.toFormat('yyyy-MM-dd');\nconst tomorrow = now.plus({ days: 1 });\n\n// $jmespath() — query JSON structures\nconst adults = $jmespath($input.first().json, 'users[?age >= `18`]');\n\n// $getWorkflowStaticData() — data that persists across executions\n```\n\n**Sandbox (since n8n v2.0, JsTaskRunnerSandbox):** the accessor is `this.helpers.httpRequest()` — the bare `$helpers` global is **undefined** here (`$helpers.httpRequest()` throws `ReferenceError`). Inside a nested async function where `this` is lost, call it as `await fn.call(this, ...)`. `this.helpers.httpRequestWithAuthentication` and `this.helpers.requestWithAuthenticationPaginated` are deny-listed (→ `UnsupportedFunctionError`); for authenticated calls use an **HTTP Request node** with the credential (preferred), a sub-workflow, or a manual `Authorization: Bearer ${token}` header on `this.helpers.httpRequest()` only when the token already flows through the workflow as data. `$env` is blocked when `N8N_BLOCK_ENV_ACCESS_IN_NODE=true`; `require()` works only for allowlisted modules. `Buffer`, `URL`, and standard JS globals (Math, JSON, Object, Array) always work.\n\n**See**: [BUILTIN_FUNCTIONS.md](BUILTIN_FUNCTIONS.md) for the complete reference — full httpRequest options, all DateTime/Luxon operations, JMESPath patterns, static-data use cases, and the sandbox-restriction details.\n\n---\n\n## Best Practices\n\n- **Validate input first** — guard for empty arrays / missing `.json` before processing.\n- **Try-catch risky work** (HTTP calls) and return an error object instead of crashing.\n- **Prefer array methods** (`filter`/`map`/`reduce`) over manual loops.\n- **Filter early, transform late** — shrink the dataset before expensive work.\n- **Descriptive names** and `console.log()` for debugging (output goes to the browser console).\n\n**See**: [COMMON_PATTERNS.md](COMMON_PATTERNS.md) → \"Best Practices\" for code examples of each.\n\n---\n\n## Production Gotchas\n\nHard-won lessons from real deployments — summarized here, with code in [DATA_ACCESS.md](DATA_ACCESS.md) → \"Production Gotchas\":\n\n- **SplitInBatches outputs are counterintuitive**: `main[0]` = **done** (fires once, after all batches), `main[1]` = **each batch** (the loop body). Add a **Limit 1** node after the done output as a safety.\n- **Iteration count is the cost**: each loop iteration re-runs the whole body through the engine (~0.8 ms overhead each). `batchSize: 1` is the loop equivalent of *Each Item* — use the largest batch your real constraint (rate limit, page size, memory) allows, or don't loop at all.\n- **Cross-iteration accumulation (CRITICAL)**: after the loop, `$('Node Inside Loop').all()` returns ONLY the last iteration's items. Accumulate via `$getWorkflowStaticData('global')` (reset before, push inside, read after).\n- **pairedItem**: when emitting items that don't map 1:1 to input, set `pairedItem: { item: i }` or downstream Set nodes fail with `paired_item_no_info`.\n- **Node reference syntax**: `$('Node').first().json` or `$('Node').all()` — never `.json` directly on the reference.\n- **Float precision**: compare currency at the cent level — `Math.round(a*100) !== Math.round(b*100)` — to avoid false positives from float noise.\n\n---\n\n## When to Use Code Node\n\n> **Before reaching for a Code node, walk the transform gatekeeper** in the n8n Expression Syntax skill: expression → arrow-function IIFE inside an Edit Fields field → Code node, in that order. The first two paths cover most \"transform this data\" tasks at ~1–10ms each, versus the Code node's sandboxed ~500–1000ms — a ~100x gap on pure single-item shaping, with no functional difference. The Code node earns its place only for whole-dataset aggregation (`$input.all()`), allowlisted libraries, or async work. And before writing code for crypto (HMAC, hashing, signing) or XML/SOAP/RSS parsing, check for a **native node** — n8n has a Crypto node (`nodes-base.crypto`) and an XML node (`nodes-base.xml`) that cover those without any JavaScript. Dropping into Code for something a native node already does is one of the most common false positives.\n\nUse Code node when:\n- ✅ Complex transformations requiring multiple steps\n- ✅ Custom calculations or business logic\n- ✅ Recursive operations\n- ✅ API response parsing with complex structure\n- ✅ Multi-step conditionals\n- ✅ Data aggregation across items\n\nConsider other nodes when:\n- ❌ Simple field mapping → Use **Set** node\n- ❌ Basic filtering → Use **Filter** node\n- ❌ Simple conditionals → Use **IF** or **Switch** node\n- ❌ HTTP requests only → Use **HTTP Request** node\n\n**Code node excels at**: Complex logic that would require chaining many simple nodes\n\n---\n\n## Integration with Other Skills\n\n### Works With:\n\n**n8n Expression Syntax**:\n- Expressions use `{{ }}` syntax in other nodes\n- Code nodes use JavaScript directly (no `{{ }}`)\n- When to use expressions vs code\n\n**n8n MCP Tools Expert**:\n- How to find Code node: `search_nodes({query: \"code\"})`\n- Get configuration help: `get_node({nodeType: \"nodes-base.code\"})`\n- Validate code: `validate_node({nodeType: \"nodes-base.code\", config: {...}})`\n\n**n8n Node Configuration**:\n- Mode selection (All Items vs Each Item)\n- Language selection (JavaScript vs Python)\n- Understanding property dependencies\n\n**n8n Workflow Patterns**:\n- Code nodes in transformation step\n- Webhook → Code → API pattern\n- Error handling in workflows\n\n**n8n Validation Expert**:\n- Validate Code node configuration\n- Handle validation errors\n- Auto-fix common issues\n\n---\n\n## Quick Reference Checklist\n\nBefore deploying Code nodes, verify:\n\n- [ ] **Code is not empty** - Must have meaningful logic\n- [ ] **Return statement exists** - Returns items, not a primitive/`null`\n- [ ] **Canonical return format** - Each item: `{json: {...}}` (bare objects auto-wrap, but be explicit)\n- [ ] **Data access correct** - Using `$input.all()`, `$input.first()`, or `$input.item`\n- [ ] **No `{{ }}` written as code** - Use JavaScript template literals: `` `${value}` ``\n- [ ] **Error handling** - Guard clauses for null/undefined inputs\n- [ ] **Webhook data** - Access via `.body` if from webhook\n- [ ] **Mode selection** - \"All Items\" for most cases\n- [ ] **Performance** - Prefer map/filter over manual loops\n- [ ] **Output consistent** - All code paths return same structure\n\n---\n\n## Additional Resources\n\n### Related Files\n- [DATA_ACCESS.md](DATA_ACCESS.md) - Comprehensive data access patterns\n- [COMMON_PATTERNS.md](COMMON_PATTERNS.md) - 10 production-tested patterns\n- [ERROR_PATTERNS.md](ERROR_PATTERNS.md) - Top 5 errors and solutions\n- [BUILTIN_FUNCTIONS.md](BUILTIN_FUNCTIONS.md) - Complete built-in reference\n\n### n8n Documentation\n- Code Node Guide: https://docs.n8n.io/code/code-node/\n- Built-in Methods: https://docs.n8n.io/code-examples/methods-variables-reference/\n- Luxon Documentation: https://moment.github.io/luxon/\n\n---\n\n**Ready to write JavaScript in n8n Code nodes!** Start with simple transformations, use the error patterns guide to avoid common mistakes, and reference the pattern library for production-ready examples.\n\n<!-- File: data/skills/n8n-code-python/COMMON_PATTERNS.md (Tokens: ~300 | Category: anthropic-skill) -->\n# N8n-code-python Skill\n\nPath: `data/skills/n8n-code-python/COMMON_PATTERNS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-code-python/COMMON_PATTERNS.md)\n\n<!-- File: data/skills/n8n-code-python/DATA_ACCESS.md (Tokens: ~300 | Category: anthropic-skill) -->\n# N8n-code-python Skill\n\nPath: `data/skills/n8n-code-python/DATA_ACCESS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-code-python/DATA_ACCESS.md)\n\n<!-- File: data/skills/n8n-code-python/ERROR_PATTERNS.md (Tokens: ~300 | Category: anthropic-skill) -->\n# N8n-code-python Skill\n\nPath: `data/skills/n8n-code-python/ERROR_PATTERNS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-code-python/ERROR_PATTERNS.md)\n\n<!-- File: data/skills/n8n-code-python/README.md (Tokens: ~300 | Category: anthropic-skill) -->\n# N8n-code-python Skill\n\nPath: `data/skills/n8n-code-python/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-code-python/README.md)\n\n<!-- File: data/skills/n8n-code-python/SKILL.md (Tokens: ~3941 | Category: anthropic-skill) -->\n---\nname: n8n-code-python\ndescription: Write Python code in n8n Code nodes. Use when writing Python in n8n, using _input/_json/_node syntax, working with standard library, or need to understand Python limitations in n8n Code nodes. Use this skill when the user specifically requests Python for an n8n Code node. Note — JavaScript is recommended for 95% of use cases — only use Python when the user explicitly prefers it or the task requires Python-specific standard library capabilities (regex, hashlib, statistics). EXCEPTION — for Python in the AI-agent-callable Custom Code Tool (@n8n/n8n-nodes-langchain.toolCode), use the n8n-code-tool skill instead (input is _query, return must be a string).\n---\n\n# Python Code Node (Beta)\n\nExpert guidance for writing Python code in n8n Code nodes.\n\n---\n\n## ⚠️ Important: JavaScript First\n\n**Recommendation**: Use **JavaScript for 95% of use cases**. Only use Python when:\n- You need specific Python standard library functions\n- You're significantly more comfortable with Python syntax\n- You're doing data transformations better suited to Python\n\n**Why JavaScript is preferred:**\n- Full n8n helper functions (`this.helpers.httpRequest`, etc.)\n- Luxon DateTime library for advanced date/time operations\n- No external library limitations\n- Better n8n documentation and community support\n\n---\n\n## Quick Start\n\n```python\n# Basic template for Python Code nodes\nitems = _input.all()\n\n# Process data\nprocessed = []\nfor item in items:\n    processed.append({\n        \"json\": {\n            **item[\"json\"],\n            \"processed\": True,\n            \"timestamp\": datetime.now().isoformat()\n        }\n    })\n\nreturn processed\n```\n\n### Essential Rules\n\n1. **Consider JavaScript first** - Use Python only when necessary\n2. **Access data**: `_input.all()`, `_input.first()`, or `_input.item`\n3. **CRITICAL**: Must return `[{\"json\": {...}}]` format\n4. **CRITICAL**: Webhook data is under `_json[\"body\"]` (not `_json` directly)\n5. **CRITICAL LIMITATION**: **No external libraries** (no requests, pandas, numpy)\n6. **Standard library only**: json, datetime, re, base64, hashlib, urllib.parse, math, random, statistics\n\n---\n\n## Mode Selection Guide\n\nSame as JavaScript - choose based on your use case:\n\n### Run Once for All Items (Recommended - Default)\n\n**Use this mode for:** 95% of use cases\n\n- **How it works**: Code executes **once** regardless of input count\n- **Data access**: `_input.all()` or `_items` array (Native mode)\n- **Best for**: Aggregation, filtering, batch processing, transformations\n- **Performance**: Faster for multiple items (single execution)\n\n```python\n# Example: Calculate total from all items\nall_items = _input.all()\ntotal = sum(item[\"json\"].get(\"amount\", 0) for item in all_items)\n\nreturn [{\n    \"json\": {\n        \"total\": total,\n        \"count\": len(all_items),\n        \"average\": total / len(all_items) if all_items else 0\n    }\n}]\n```\n\n### Run Once for Each Item\n\n**Use this mode for:** Specialized cases only\n\n- **How it works**: Code executes **separately** for each input item\n- **Data access**: `_input.item` or `_item` (Native mode)\n- **Best for**: Item-specific logic, independent operations, per-item validation\n- **Performance**: Slower for large datasets (multiple executions)\n\n```python\n# Example: Add processing timestamp to each item\nitem = _input.item\n\nreturn [{\n    \"json\": {\n        **item[\"json\"],\n        \"processed\": True,\n        \"processed_at\": datetime.now().isoformat()\n    }\n}]\n```\n\n---\n\n## Python Modes: Beta vs Native\n\nn8n offers two Python execution modes:\n\n### Python (Beta) - Recommended\n- **Use**: `_input`, `_json`, `_node` helper syntax\n- **Best for**: Most Python use cases\n- **Helpers available**: `_now`, `_today`, `_jmespath()`\n- **Import**: `from datetime import datetime`\n\n```python\n# Python (Beta) example\nitems = _input.all()\nnow = _now  # Built-in datetime object\n\nreturn [{\n    \"json\": {\n        \"count\": len(items),\n        \"timestamp\": now.isoformat()\n    }\n}]\n```\n\n### Python (Native) (Beta)\n- **Use**: `_items`, `_item` variables only\n- **No helpers**: No `_input`, `_now`, etc.\n- **More limited**: Standard Python only\n- **Use when**: Need pure Python without n8n helpers\n\n```python\n# Python (Native) example\nprocessed = []\n\nfor item in _items:\n    processed.append({\n        \"json\": {\n            \"id\": item[\"json\"].get(\"id\"),\n            \"processed\": True\n        }\n    })\n\nreturn processed\n```\n\n**Recommendation**: Use **Python (Beta)** for better n8n integration.\n\n---\n\n## Data Access Patterns\n\nAccess input data through underscore-prefixed variables. Each item is a dict shaped `{\"json\": {...}}`, so the actual fields live under `[\"json\"]`.\n\n```python\n# Pattern 1: _input.all() - Most common. Arrays, batch ops, aggregations\nall_items = _input.all()            # list of {\"json\": {...}} dicts\n\n# Pattern 2: _input.first() - Very common. Single objects, API responses\ndata = _input.first()[\"json\"]       # built-in safety vs all_items[0]\n\n# Pattern 3: _input.item - \"Run Once for Each Item\" mode ONLY\ncurrent = _input.item[\"json\"]       # None/error in All Items mode\n\n# Pattern 4: _node - Reference a specific named node\nwebhook_data = _node[\"Webhook\"][\"json\"]\nhttp_data = _node[\"HTTP Request\"][\"json\"]\n```\n\n**See**: [DATA_ACCESS.md](DATA_ACCESS.md) for the comprehensive guide — six `_input.all()` recipes (filter, transform, aggregate, sort, group, deduplicate), `_input.first()` and `_input.item` examples, multi-node combining, the JS-vs-Python variable table, and the decision tree.\n\n---\n\n## Critical: Webhook Data Structure\n\n**MOST COMMON MISTAKE**: Webhook data is nested under `[\"body\"]`\n\n```python\n# ❌ WRONG - Will raise KeyError\nname = _json[\"name\"]\nemail = _json[\"email\"]\n\n# ✅ CORRECT - Webhook data is under [\"body\"]\nname = _json[\"body\"][\"name\"]\nemail = _json[\"body\"][\"email\"]\n\n# ✅ SAFER - Use .get() for safe access\nwebhook_data = _json.get(\"body\", {})\nname = webhook_data.get(\"name\")\n```\n\n**Why**: Webhook node wraps all request data under `body` property. This includes POST data, query parameters, and JSON payloads.\n\n**See**: [DATA_ACCESS.md](DATA_ACCESS.md) for full webhook structure details\n\n---\n\n## Return Format Requirements\n\n**CRITICAL RULE**: Always return list of dictionaries with `\"json\"` key\n\n### Correct Return Formats\n\n```python\n# ✅ Single result\nreturn [{\n    \"json\": {\n        \"field1\": value1,\n        \"field2\": value2\n    }\n}]\n\n# ✅ Multiple results\nreturn [\n    {\"json\": {\"id\": 1, \"data\": \"first\"}},\n    {\"json\": {\"id\": 2, \"data\": \"second\"}}\n]\n\n# ✅ List comprehension\ntransformed = [\n    {\"json\": {\"id\": item[\"json\"][\"id\"], \"processed\": True}}\n    for item in _input.all()\n    if item[\"json\"].get(\"valid\")\n]\nreturn transformed\n\n# ✅ Empty result (when no data to return)\nreturn []\n\n# ✅ Conditional return\nif should_process:\n    return [{\"json\": processed_data}]\nelse:\n    return []\n```\n\n### Incorrect Return Formats\n\n```python\n# ❌ WRONG: Dictionary without list wrapper\nreturn {\n    \"json\": {\"field\": value}\n}\n\n# ❌ WRONG: List without json wrapper\nreturn [{\"field\": value}]\n\n# ❌ WRONG: Plain string\nreturn \"processed\"\n\n# ❌ WRONG: Incomplete structure\nreturn [{\"data\": value}]  # Should be {\"json\": value}\n```\n\n**Why it matters**: Next nodes expect list format. Incorrect format causes workflow execution to fail.\n\n**See**: [ERROR_PATTERNS.md](ERROR_PATTERNS.md) #2 for detailed error solutions\n\n---\n\n## Critical Limitation: No External Libraries\n\n**MOST IMPORTANT PYTHON LIMITATION**: Cannot import external packages on default installs.\n\n> **Self-hosted exception**: external package availability depends entirely on the instance's Python runner configuration. If the user states their self-hosted instance has specific packages available in the Python runner environment, use them — don't refuse. When unsure, ask or write standard-library-only code.\n\n**❌ NOT available** (raise `ModuleNotFoundError`): `requests`, `pandas`, `numpy`, `scipy`, `bs4`/BeautifulSoup, `lxml`.\n\n**✅ Available** (standard library only): `json`, `datetime`, `re`, `base64`, `hashlib`, `urllib.parse`, `math`, `random`, `statistics`.\n\n### Workarounds\n\n**Need HTTP requests?**\n- ✅ Use **HTTP Request node** before Code node\n- ✅ Or switch to **JavaScript** and use `this.helpers.httpRequest()` (the bare `$helpers` global is undefined in the task-runner sandbox)\n\n**Need data analysis (pandas/numpy)?**\n- ✅ Use Python **statistics** module for basic stats\n- ✅ Or switch to **JavaScript** for most operations\n- ✅ Manual calculations with lists and dictionaries\n\n**Need web scraping (BeautifulSoup)?**\n- ✅ Use **HTTP Request node** + **HTML Extract node**\n- ✅ Or switch to **JavaScript** with regex/string methods\n\n**See**: [STANDARD_LIBRARY.md](STANDARD_LIBRARY.md) for complete reference\n\n---\n\n## Common Patterns Overview\n\nBased on production workflows, the most useful Python patterns are:\n\n1. **Data Transformation** - Transform all items with list comprehensions\n2. **Filtering & Aggregation** - Sum, filter, count with built-in functions\n3. **String Processing with Regex** - Extract patterns from text with `re`\n4. **Data Validation** - Validate and clean data, attach error lists\n5. **Statistical Analysis** - Calculate mean/median/stdev with the `statistics` module\n\nCopy-ready snippets for all five live in [COMMON_PATTERNS.md](COMMON_PATTERNS.md#quick-pattern-snippets), alongside 10 fully detailed production patterns (multi-source aggregation, markdown parsing, JSON comparison, CRM normalization, dictionary lookup, top-N filtering, and more).\n\n---\n\n## Error Prevention - Top 5 Mistakes\n\n1. **Importing external libraries** (Python-specific) → `import requests` raises `ModuleNotFoundError`. Use the HTTP Request node or JavaScript instead.\n2. **Empty code or missing return** → every path must end with `return [{\"json\": ...}]`.\n3. **Incorrect return format** → wrap in a list: `{\"json\": {...}}` becomes `[{\"json\": {...}}]`.\n4. **KeyError on dictionary access** → use `.get()`: `_json.get(\"user\", {}).get(\"name\", \"Unknown\")`.\n5. **Webhook body nesting** → read via `[\"body\"]`: `_json.get(\"body\", {}).get(\"email\", \"no-email\")`.\n\n**See**: [ERROR_PATTERNS.md](ERROR_PATTERNS.md) for the comprehensive guide — each error with wrong-vs-right code, error messages, nested-access fixes, an `AttributeError` bonus case, a prevention checklist, and a quick-fix table.\n\n---\n\n## Standard Library Reference\n\nMost useful modules: `json` (parse/generate), `datetime` (dates + `timedelta`), `re` (regex), `base64` (encode/decode), `hashlib` (hashing), `urllib.parse` (URL ops), and `statistics` (mean/median/stdev). Also available: `math`, `random`, `collections`, `itertools`, `functools`.\n\nFor a condensed cheat sheet plus full per-module examples, see [STANDARD_LIBRARY.md](STANDARD_LIBRARY.md#quick-reference-most-useful-modules).\n\n---\n\n## Best Practices\n\n### 1. Always Use .get() for Dictionary Access\n\n```python\n# ✅ SAFE: Won't crash if field missing\nvalue = item[\"json\"].get(\"field\", \"default\")\n\n# ❌ RISKY: Crashes if field doesn't exist\nvalue = item[\"json\"][\"field\"]\n```\n\n### 2. Handle None/Null Values Explicitly\n\n```python\n# ✅ GOOD: Default to 0 if None\namount = item[\"json\"].get(\"amount\") or 0\n\n# ✅ GOOD: Check for None explicitly\ntext = item[\"json\"].get(\"text\")\nif text is None:\n    text = \"\"\n```\n\n### 3. Use List Comprehensions for Filtering\n\n```python\n# ✅ PYTHONIC: List comprehension\nvalid = [item for item in items if item[\"json\"].get(\"active\")]\n\n# ❌ VERBOSE: Manual loop\nvalid = []\nfor item in items:\n    if item[\"json\"].get(\"active\"):\n        valid.append(item)\n```\n\n### 4. Return Consistent Structure\n\n```python\n# ✅ CONSISTENT: Always list with \"json\" key\nreturn [{\"json\": result}]  # Single result\nreturn results  # Multiple results (already formatted)\nreturn []  # No results\n```\n\n### 5. Debug with print() Statements\n\n```python\n# Debug statements appear in browser console (F12)\nitems = _input.all()\nprint(f\"Processing {len(items)} items\")\nprint(f\"First item: {items[0] if items else 'None'}\")\n```\n\n---\n\n## Production Gotchas\n\n### SplitInBatches Loop Semantics\n\nThe SplitInBatches node has two outputs:\n- `main[0]` = **done** — fires ONCE after all batches complete\n- `main[1]` = **each batch** — fires for every batch (the loop body)\n\nAlways add a **Limit 1** node after the done output.\n\n### Correct Node Reference Syntax\n\n```python\n# ❌ WRONG\ndata = _node['HTTP Request']['json']\n\n# ✅ CORRECT - call .first() then access json\ndata = _node['HTTP Request'].first()['json']\n```\n\n### Cross-Iteration Data Not Available in Python\n\n`$getWorkflowStaticData('global')` may not be available in Python Beta mode. If you need to accumulate data across SplitInBatches iterations, use a JavaScript Code node for the accumulation logic instead.\n\n---\n\n## When to Use Python vs JavaScript\n\n### Use Python When:\n- ✅ You need `statistics` module for statistical operations\n- ✅ You're significantly more comfortable with Python syntax\n- ✅ Your logic maps well to list comprehensions\n- ✅ You need specific standard library functions\n\n### Use JavaScript When:\n- ✅ You need HTTP requests (`this.helpers.httpRequest()`)\n- ✅ You need advanced date/time (DateTime/Luxon)\n- ✅ You want better n8n integration\n- ✅ **For 95% of use cases** (recommended)\n\n### Consider Other Nodes When:\n- ❌ Simple field mapping → Use **Set** node\n- ❌ Basic filtering → Use **Filter** node\n- ❌ Simple conditionals → Use **IF** or **Switch** node\n- ❌ HTTP requests only → Use **HTTP Request** node\n\n---\n\n## Integration with Other Skills\n\n### Works With:\n\n**n8n Expression Syntax**:\n- Expressions use `{{ }}` syntax in other nodes\n- Code nodes use Python directly (no `{{ }}`)\n- When to use expressions vs code\n\n**n8n MCP Tools Expert**:\n- How to find Code node: `search_nodes({query: \"code\"})`\n- Get configuration help: `get_node({nodeType: \"nodes-base.code\"})`\n- Validate code: `validate_node({nodeType: \"nodes-base.code\", config: {...}})`\n\n**n8n Node Configuration**:\n- Mode selection (All Items vs Each Item)\n- Language selection (Python vs JavaScript)\n- Understanding property dependencies\n\n**n8n Workflow Patterns**:\n- Code nodes in transformation step\n- When to use Python vs JavaScript in patterns\n\n**n8n Validation Expert**:\n- Validate Code node configuration\n- Handle validation errors\n- Auto-fix common issues\n\n**n8n Code JavaScript**:\n- When to use JavaScript instead\n- Comparison of JavaScript vs Python features\n- Migration from Python to JavaScript\n\n---\n\n## Quick Reference Checklist\n\nBefore deploying Python Code nodes, verify:\n\n- [ ] **Considered JavaScript first** - Using Python only when necessary\n- [ ] **Code is not empty** - Must have meaningful logic\n- [ ] **Return statement exists** - Must return list of dictionaries\n- [ ] **Proper return format** - Each item: `{\"json\": {...}}`\n- [ ] **Data access correct** - Using `_input.all()`, `_input.first()`, or `_input.item`\n- [ ] **No external imports** - Only standard library (json, datetime, re, etc.)\n- [ ] **Safe dictionary access** - Using `.get()` to avoid KeyError\n- [ ] **Webhook data** - Access via `[\"body\"]` if from webhook\n- [ ] **Mode selection** - \"All Items\" for most cases\n- [ ] **Output consistent** - All code paths return same structure\n\n---\n\n## Additional Resources\n\n### Related Files\n- [DATA_ACCESS.md](DATA_ACCESS.md) - Comprehensive Python data access patterns\n- [COMMON_PATTERNS.md](COMMON_PATTERNS.md) - 10 Python patterns for n8n\n- [ERROR_PATTERNS.md](ERROR_PATTERNS.md) - Top 5 errors and solutions\n- [STANDARD_LIBRARY.md](STANDARD_LIBRARY.md) - Complete standard library reference\n\n### n8n Documentation\n- Code Node Guide: https://docs.n8n.io/code/code-node/\n- Python in n8n: https://docs.n8n.io/code/builtin/python-modules/\n\n---\n\n**Ready to write Python in n8n Code nodes - but consider JavaScript first!** Use Python for specific needs, reference the error patterns guide to avoid common mistakes, and leverage the standard library effectively.\n\n<!-- File: data/skills/n8n-code-python/STANDARD_LIBRARY.md (Tokens: ~300 | Category: anthropic-skill) -->\n# N8n-code-python Skill\n\nPath: `data/skills/n8n-code-python/STANDARD_LIBRARY.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/czlonkowski/n8n-mcp/HEAD/data/skills/n8n-code-python/STANDARD_LIBRARY.md)\n\n<!-- ... +61 more modular skills available via /api/skills/czlonkowski/n8n-mcp?path={path} -->\n</yakaai_skills>"}