data/skills/n8n-validation-expert/REVIEW_CHECKLIST.md
A severity-tiered audit for reviewing an existing n8n workflow — yours or anyone's. This is different from the validate-as-you-build loop in the main skill: that loop catches schema and shape errors with validate_node / validate_workflow; this checklist catches the silent issues those tools pass clean — antipatterns, security holes, broken-but-valid connections, and missing error paths.
Pull the workflow first, then walk the list top to bottom. For each item, inspect the actual JSON and decide if it applies. Report findings grouped by severity, each pointing at the canonical skill for the why and the fix.
You're reviewing JSON, not source. n8n_get_workflow returns nodes (with parameters, credentials, type strings like nodes-base.httpRequest) and a connections graph. Phrase findings in JSON terms: "node Route order has no parameters.options.fallbackOutput, so unmatched items drop."
Bare NODE_FAMILY_GOTCHAS.md references in the lists below all point to n8n-node-configuration/NODE_FAMILY_GOTCHAS.md.
| Severity | Meaning | Action |
|---|---|---|
| MUST FIX | Ship-blocker: security hole, broken connection, production-breaking bug. | If the workflow is active, stop it; fix before re-enabling. |
| SHOULD FIX | Real issue: antipattern, missing error handling on a production path, broken contract. | Fix in the next change. |
| NICE TO HAVE | Polish: naming, descriptions, readability. | Clean up opportunistically. |
A review agent should not auto-fix MUST FIX items without user confirmation — security and connection changes have blast radius. Surface the finding, propose the fix, wait for approval.
n8n_get_workflow({ id }) so every check runs on real JSON, not assumptions. Use structure mode for a fast graph read, full when you need parameters, and filtered + nodeNames to read a single heavy node (e.g. a long Code node) on a large workflow that would otherwise truncate client-side when fetched whole.Bearer xxx, sk-... typed into an HTTP header value, a query param, or any parameter). The credential system is the only correct home. Use n8n_manage_credentials to inspect/migrate. → n8n-mcp-tools-expert (credential management){{ $json.token }} referencing. The secret is in the workflow JSON regardless of how it's read. → n8n-mcp-tools-expert"id": "REPLACE_ME") left in the credentials block. n8n renders a permanently disabled selector for unknown IDs. Omit the block when the real ID is unknown. → n8n-node-configurationRun
n8n_audit_instanceto surface hardcoded secrets and unauthenticated webhooks across the instance automatically. → n8n-validation-expert
{{ ... }} inside parameters.query — n8n substitutes it into the SQL before the driver binds parameters, so it's an injection vector. Use $1, $2 placeholders + parameters.options.queryReplacement (Postgres/MySQL); object filters for Mongo. → n8n-node-configuration → NODE_FAMILY_GOTCHAS.md (Database)numberOfInputs still 2. Third source silently drops. → n8n-node-configuration → NODE_FAMILY_GOTCHAS.md (Merge)parameters.useDataOfInput is 1-indexed; the wire sits at connections.<source>.main[N-1]. Mismatch passes through the wrong source silently. Verify with n8n_get_workflow. → n8n-node-configuration → NODE_FAMILY_GOTCHAS.md (Merge)parameters.onError is continueErrorOutput but the node's error output is empty, failed items are silently dropped; if a node feeds an error branch but onError isn't set, the branch is unreachable and a failure halts the workflow. validate_workflow now surfaces both as warnings (n8n-mcp ≥ 2.63.0), but neither flips valid:false, so this stays a review item. Locate the error output by node type: it is the last main[] slot after the node's natural outputs — main[1] on a single-output node like HTTP Request, but main[2] on an IF (whose main[1] is the normal false branch) and similarly on Switch/Split In Batches. Don't mistake a wired second branch on a multi-output node for an unwired error output. → n8n-validation-expertparameters.options.fallbackOutput: "extra", unmatched items drop silently. → n8n-node-configuration → NODE_FAMILY_GOTCHAS.md (Switch)parameters.authentication: "none". "Sensitive" = mutates state, sends external messages, hits production data, triggers paid actions. Anyone with the URL can fire it. Set authentication to basicAuth/headerAuth with a matching credential. → n8n-workflow-patterns (webhook), n8n-mcp-tools-expert (credentials)responseCode defaults to 200 on every Respond node, including error paths. The caller sees success while the body says failure. → n8n-node-configuration → NODE_FAMILY_GOTCHAS.md (Webhook).map/.filter/.find, field rename, optional chaining). Use an expression or an Edit Fields arrow-function IIFE — same result, ~100x faster, more readable. → n8n-code-javascript (the transform gatekeeper)crypto.createHash / crypto.createHmac. Use the native Crypto node (nodes-base.crypto). Recurring slip. → n8n-code-javascriptnodes-base.xml) + Edit Fields for extraction. → n8n-code-javascript$json.x deep in a branchy / multi-step workflow. Switch to $('Source Node').item.json.x for refactor stability; the $json form breaks silently when an intermediate is inserted or context is cleared. → n8n-expression-syntax (non-negotiable)$json references downstream. Whichever branch fired last wins, non-deterministically. Insert a NoOp (Combine Inputs) at the merge and reference it by name; use a Set to normalize if the branch shapes differ. → n8n-expression-syntaxDateTime.fromISO(...).toFormat(...)). → n8n-expression-syntax$env.X in any expression. Doesn't work, throws at runtime. Use $vars.X (paid plans), a Data Table, or a credential for secrets. → n8n-expression-syntax.all().map()/filter()/reduce() aggregation without executeOnce: true on the node. It re-runs the full aggregation per input item — wasted work, and N identical outputs where one was expected. (Leave executeOnce off when .all() is a per-item lookup keyed by the current item.) → n8n-expression-syntax={{ { "blocks": ... } }}. → n8n-node-configuration → NODE_FAMILY_GOTCHAS.md (Slack)thread_ts not set or in the wrong place. → n8n-node-configuration → NODE_FAMILY_GOTCHAS.md (Slack)"send") rather than the internal value ("post"). Confirm with get_node. → n8n-node-configurationselect / query with no-match path feeding an IF, but alwaysOutputData not set. No match = zero items = the IF never fires. → n8n-node-configuration → NODE_FAMILY_GOTCHAS.md (Database)executeQuery with options.queryBatching: "transaction". → n8n-node-configuration → NODE_FAMILY_GOTCHAS.md (Database)alwaysOutputData. Writes often return 0 items and stall the chain. → n8n-node-configurationresponseMode left at onReceived for a request/response API. Caller never sees the computed result; use responseNode. → n8n-node-configuration → NODE_FAMILY_GOTCHAS.md (Webhook)respondWith: "json" body built with JSON.stringify(...). Double-encodes. Pass the object literal in expression mode. → n8n-node-configuration → NODE_FAMILY_GOTCHAS.md (Webhook)onError: "continueErrorOutput" + a 5xx Respond, and validate the wiring. → n8n-error-handling, n8n-workflow-patterns (webhook)headerParameters instead of a credential. Use Bearer Auth / Header Auth credentials. → n8n-node-configuration, n8n-mcp-tools-expertheaderParameters and a credential's header auth. They conflict. → n8n-node-configurationretryOnFail. Transient 429s and blips surface as hard failures. (Transient-failure handling folds into node config for now.) → n8n-node-configuration[{json:{...}}]) or using $fromAI / $input / $helpers — none exist in the Code Tool sandbox; the return must be a string. → n8n-code-toolHTTP Request1, Set2, Postgres1). A runtime failure on Fetch order details localizes the break instantly; HTTP Request3 tells the operator nothing. Rename to describe what the node does in this workflow. → n8n-workflow-patternsSend weekly customer report, not Customer report sender). Sentence case, no emojis, no trailing version numbers. → n8n-workflow-patternsdescription empty or one line. Two sentences: what it does and why it exists (the "why" is the part that otherwise gets lost). → n8n-workflow-patternsGroup by severity, then by domain. For each finding give the node(s) affected, a one-sentence description, and the canonical skill for the fix.
MUST FIX
Security
- Node `Send webhook`: bearer token typed into headerParameters value. -> n8n-mcp-tools-expert (credentials)
- Node `Lookup user`: {{ $json.email }} interpolated into parameters.query. -> NODE_FAMILY_GOTCHAS.md (Database)
Connections
- Node `Merge customer + Stripe`: 3 sources wired but parameters.numberOfInputs = 2; third drops. -> NODE_FAMILY_GOTCHAS.md (Merge)
SHOULD FIX
Set-node antipattern
- Node `Set customer_id`: feeds one consumer; inline at `Lookup customer`. -> n8n-expression-syntax
...