Communityn8ngithub.com

ashtonfeliciano/n8n-workflow-engineer

Build, debug, and harden n8n workflows. Use when the user is editing n8n workflow JSON, debugging silent failures, working with Code nodes, Telegram / Gmail / HTTP Request nodes, scheduled triggers, paired-item tracking, dedup with static data, or doing bulk operations via the n8n REST API. Covers v2.x UI specifics. Not for non-n8n automation.

What is n8n-workflow-engineer?

n8n-workflow-engineer is a Claude Code agent skill that build, debug, and harden n8n workflows. Use when the user is editing n8n workflow JSON, debugging silent failures, working with Code nodes, Telegram / Gmail / HTTP Request nodes, scheduled triggers, paired-item tracking, dedup with static data, or doing bulk operations via the n8n REST API. Covers v2.x UI specifics. Not for non-n8n automation.

Works with✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/ashtonfeliciano/claude-skills-pack/tree/HEAD/skills/n8n-workflow-engineer

Installed? Explore more n8n skills: ashtonfeliciano/n8n-evaluations-runner, hubgunter4-ops/configurador-de-nodos-n8n, hubgunter4-ops/experto-en-n8n-mcp · View all 6 →

Ask in your favorite AI

Open a new chat with this agent skill pre-loaded.

Documentation

n8n Workflow Engineer

When to trigger

  • Editing or building n8n workflow JSON
  • Debugging n8n: "node not executing", "paired item unavailable", "static data not persisting"
  • Bulk operations (rename, retry, delete) via n8n REST API
  • Telegram, Gmail, HTTP Request, Code, Switch, Merge node patterns
  • Scheduled trigger / timezone / cron behavior in n8n
  • n8n v2.x UI behavior (Publish/Unpublish, Archive)

Hard rules

  1. Audit before edit. Read the full workflow JSON → present findings → confirm → apply in one pass. Never patch blindly.
  2. Test via scheduled trigger. Manual "Execute" resets $workflow.staticData. Dedup proof requires a real cron run.
  3. Verify the real result. Check the execution log and the end state (the message arrived, the row exists). A node returning ok: true is a good sign, not proof. UI sync lag fools observers.
  4. Match Code node mode to return signature. Wrong mode + wrong return = silent partial processing.

Quick reference — the most common bugs

SymptomRoot causeFix
HTTP Request to localhost:4000 returns refused/timeoutn8n in Docker bridge modeUse the host gateway: often 172.17.0.1 on the default bridge (check with docker network inspect bridge), or map host.docker.internal:host-gateway in extra_hosts
Telegram message never arrives, no errorIPv6 DNS resolution timeoutNODE_OPTIONS=--dns-result-order=ipv4first in docker-compose
new Date() returns UTC despite cron in ESTContainer missing TZ env varSet both TZ AND GENERIC_TIMEZONE to your time zone
parse_mode: Markdown fails silently or garblesUnmatched * or _ in AI outputUse parse_mode: HTML + sanitize before send
401 on LiteLLM HTTP Request nodeMissing auth headerAdd Authorization: Bearer {{ $env.LITELLM_API_KEY }}
ECONNABORTED: timeout of 15000ms on Claude callDefault 15s timeoutSet options.timeout: 90000
PUT to /api/v1/workflows/:id returns 400Extra fields in bodyStrip to: name, nodes, connections, settings, staticData only
Node rename breaks workflow silentlyConnection refs reference old nameReplace_all the old name in both nodes[].name AND connections{} keys
Paired item unavailable after Code aggregationN→1→M lost item lineageSet executeOnce: true, iterate $('UpstreamNode').all() explicitly
Static data dedup "not working" on manual runManual Execute resets static dataTest via scheduled cron only

Code node — the mode/return rules

Every Code node has a mode parameter. Mismatch = silent bugs.

ModeAccess patternReturn
runOnceForAllItems (default)const items = $input.all();return items.map(...) — must be array
runOnceForEachItem$json is current itemreturn { json: ... } — single object, NOT array

Symptoms of mismatch:

  • runOnceForAllItems + accessing $json → only first item processed silently
  • runOnceForEachItem + returning array → "A 'json' property isn't an object" validation error

Dedup pattern (cross-run state)

const store = $getWorkflowStaticData('global');
store.jobIds = store.jobIds || [];

const newItems = items.filter(item => !store.jobIds.includes(item.json.id));

// Persist new IDs, cap to last 500 to avoid unbounded growth
store.jobIds = [...store.jobIds, ...newItems.map(i => i.json.id)].slice(-500);

return newItems;

Critical: staticData persists across scheduled runs only. Manual Execute resets it. Verify dedup by watching two consecutive scheduled runs, not by clicking Execute twice.

Per-run cache cleanup

// At top of first Code node — clean transient caches but preserve cross-run state
const store = $getWorkflowStaticData('global');
for (const k of Object.keys(store)) {
  if (k.startsWith('cjob_') || k.startsWith('cprep_') || k === 'pendingJobs') {
    delete store[k];
  }
}
// NEVER delete jobIds — cross-run dedup depends on it

Pipeline observability pattern

Set counters at each major Code node. Read at the formatting step. Append to message.

// Stage 1 — Build Tickers
const store = $getWorkflowStaticData('global');
store.pipeline = { ticker_count: tickers.length, fixed_count: fixed.length };

// Stage 2 — Calculate Signals
store.pipeline = store.pipeline || {};
store.pipeline.signal_count = results.length;

// Final — Format message
const pl = store.pipeline || {};
const pf = (n) => (n === undefined || n === null) ? '?' : String(n);
const pipelineBlock =
  '\n\n📊 Pipeline\n' +
  'Build Tickers:   ' + pf(pl.ticker_count) + '\n' +
  'Signals:         ' + pf(pl.signal_count);

Paired-item issue + workaround

Symptom: After an aggregation node (N items → 1 prompt → 1 LLM response → split to M items), downstream $('UpstreamNode').item.json throws Paired item data for item from node 'X' is unavailable.

Workarounds (in order of preference):

  1. Set executeOnce: true on the downstream Code node, iterate explicitly:

    const upstream = $('UpstreamNode').all();
    const outputs = [];
    for (let i = 0; i < parsed.length; i++) {
      outputs.push({ json: { ...parsed[i], original: upstream[i].json } });
    }
    return outputs;
    
  2. Pass IDs through static data; look them up explicitly downstream.

  3. pairedItem.sourceOverwrite — fragile, last resort.

Telegram patterns

HTML mode — only 7 tags allowed

<b>, <i>, <u>, <s>, <a>, <code>, <pre> — anything else throws 400 Bad Request: Unsupported start tag.

Three-layer defense for AI-generated content

  1. Prompt: tell the AI explicitly which tags are allowed
  2. Sanitize: strip unsupported tags in code before send
  3. Escape: HTML-escape user/AI content (& → &amp;, < → &lt;, > → &gt;) before embedding

Suppress n8n attribution footer

"additionalFields": {
  "parse_mode": "HTML",
  "appendAttribution": false,
  "disable_web_page_preview": true
}

Token-in-.env vs credential

Prefer the env-var + HTTP Request pattern over the built-in Telegram node + credential:

  • Portable across migrations
  • Easier to rotate (nano .env + container restart)
  • No DB-encryption-key lockout risk
  • Pattern: POST https://api.telegram.org/bot{{ $env.BOT_TOKEN }}/sendMessage
  • Tradeoff: the token sits in the request URL, so it can appear in execution data, proxy logs and error messages. Turn off saving of execution data for this workflow, or use the built-in Telegram credential if others can read your logs.

Gmail OAuth patterns

IssueResolution
invalid_grant ... refresh token revokedOAuth in "Testing" mode expires ~7 days. Re-auth via UI, or publish app to Production.
Empty payload.headers (From/Subject blank)Default simple: true strips headers. Set options.simple: false.
Missing security alerts / receiptsDon't exclude -category:promotions -category:social -category:updates — Gmail routes 90% of relevant mail there. Use is:unread newer_than:Nd only.
Need to mark read across multiple accountsNeed per-account credentials. One Gmail credential cannot operate on another account's message IDs.

REST API patterns (bulk operations)

Auth: X-N8N-API-KEY: <key> header.

EndpointNotes
GET /api/v1/workflowsReturns all (paginated). Filter client-side.
GET /api/v1/workflows/:idFull workflow JSON.
PUT /api/v1/workflows/:idStrict: only name, nodes, connections, settings, staticData. settings itself has an allowlist (see below). Strip everything else from a GET-then-PUT round trip.
DELETE /api/v1/workflows/:idActually deletes (UI only offers Archive). Use for bulk cleanup.
POST /api/v1/workflows/:id/activateSame as Publish in v2.x UI.

settings field allowlist (PUT body)

executionOrder, timezone, saveExecutionProgress, saveDataErrorExecution, saveDataSuccessExecution, saveManualExecutions, callerPolicy, callerIds, errorWorkflow, executionTimeout

Node type is immutable post-creation

A PUT that changes node.type returns 200 but the type stays the same. To convert (e.g. telegram → httpRequest): replace the entire node object (new UUID for id, same name). Connections route by name, so name-preserving replacement is safe.

n8n v2.x UI specifics

Old (v1.x)New (v2.x)
Activate togglePublish (green button, top right) / Unpublish
Delete workflowArchive only (use REST API for real delete)

HTTP Request node — must-haves for LLM calls

{
  "method": "POST",
  "url": "http://172.17.0.1:4000/v1/chat/completions",
  "sendHeaders": true,
  "headerParameters": {
    "parameters": [
      {"name": "Content-Type", "value": "application/json"},
      {"name": "Authorization", "value": "=Bearer {{ $env.LITELLM_API_KEY }}"}
    ]
  },
  "sendBody": true,
  "contentType": "raw",
  "rawContentType": "application/json",
  "body": "={{ JSON.stringify({model:'<your-model-alias>',messages:[{role:'user',content:$json.prompt}],temperature:0.3}) }}",
  "options": {
    "timeout": 90000,
    "retry": { "tries": 2, "waitBetweenTries": 5000 }
  }
}

n8n container has no curl

For in-container HTTP tests, use Node's built-in https module via docker exec:

docker exec n8n node -e "const https=require('https'); /* request */"

SSH pre-flight (before any heredoc on VPS)

ssh -o ServerAliveInterval=30 -o ServerAliveCountMax=10 <user>@<host>
# Verify prompt: [<user>@<host> ...]$
# Only THEN paste the heredoc

Pasting a bash heredoc into PowerShell after an SSH drop = hundreds of parse errors.

Eval suite pattern (logic-only)

Mock-input tests via the n8n Evaluations feature catch logic regressions without burning API credits. Re-run before AND after any change to filter logic, keyword lists, or validation rules.

Anti-patterns

Anti-patternWhat to do instead
"Run it again, see if it works"Read execution log end-to-end first
Patching one node at a time over many round tripsAudit full file → bundled patch script → verify in one pass
Using parse_mode: Markdown for AI outputparse_mode: HTML + sanitizer
Hardcoding API keys / chat IDs in workflow JSONReference {{ $env.KEY }}, store in <secret-file> (chmod 600)
Renaming nodes without updating connectionsreplace_all on the node name in both nodes[].name and connections{} keys
Manual Execute to test dedupWait for scheduled trigger
Default httpRequest timeout for LLM callsSet 90000ms minimum
Mixing runOnceForAllItems and $jsonMatch mode to access pattern

Individual skills in this repo

This repo contains 1 individual skill — each has its own dedicated page.

Related Skills

ashtonfeliciano/n8n-evaluations-runner

Build and run zero-API-cost logic test suites for n8n workflows. Use when adding test coverage to filter logic, scoring functions, validators, keyword lists, resume maps, or any pure-JS code-node logic. Outputs grouped pass/fail/pending report via Telegram. Run before AND after any logic change to catch regressions without burning API credits.

community

hubgunter4-ops/configurador-de-nodos-n8n

Ayuda a configurar nodos de n8n con reglas de dependencia complejas y conexiones de IA. Agiliza la creación de flujos de trabajo automatizados gestionando dependencias de nodos intrincadas e integraciones de IA.

community

hubgunter4-ops/experto-en-n8n-mcp

Guía a los desarrolladores a seleccionar y configurar herramientas y formatos de nodo del Protocolo de Contexto de Modelo MCP dentro de los flujos de trabajo de n8n. Proporciona orientación estructurada para integrar herramientas MCP.

community

caiqueDeOliveira-dev/homelab

HomeLab Agent — Gerencia Home Assistant, ESP32 satellites, dispositivos Zigbee/Matter/Thread, automações físicas, cameras, sensores. Camada de decisão e execução no hardware (não orquestração de software = Automação/n8n).

community

hubgunter4-ops/experto-en-n8n-mcp

Guía a los desarrolladores a seleccionar y configurar herramientas y formatos de nodo del Protocolo de Contexto de Modelo MCP dentro de los flujos de trabajo de n8n. Proporciona orientación estructurada para integrar herramientas MCP.

community

hubgunter4-ops/experto-en-validacion-de-n8n

Corrige errores de validación de n8n consultando un catálogo de errores integrado. Optimiza el proceso de depuración, permitiendo identificar rápidamente problemas y resolver problemas de configuración del flujo de trabajo.

community