n8n Workflow Engineer
Quando acionar
- Editando ou construindo JSON de workflow n8n
- Depurando n8n: "node not executing", "paired item unavailable", "static data not persisting"
- Operações em lote (renomear, repetir, excluir) via REST API n8n
- Padrões de nós Telegram, Gmail, HTTP Request, Code, Switch, Merge
- Comportamento de trigger agendado / fuso horário / cron no n8n
- Comportamento da UI v2.x do n8n (Publicar/Cancelar publicação, Arquivar)
Regras rígidas
- Auditar antes de editar. Leia o JSON completo do workflow → apresente as descobertas → confirme → aplique em uma única passada. Nunca corrija cegamente.
- Testar via trigger agendado. A execução manual reseta
$workflow.staticData. A prova de dedup requer uma execução cron real. - Verificar o resultado real. Confira o log de execução e o estado final (a mensagem chegou, a linha existe). Um nó retornar
ok: trueé um bom sinal, não uma prova. A latência de sincronização da UI engana observadores. - Combinar o modo do nó Code com a assinatura de retorno. Modo errado + retorno errado = processamento parcial silencioso.
Referência rápida — os bugs mais comuns
| Sintoma | Causa raiz | Correção |
|---|---|---|
HTTP Request para localhost:4000 retorna recusado/timeout | n8n no modo bridge Docker | Use o gateway do host: geralmente 172.17.0.1 na bridge padrão (verifique com docker network inspect bridge), ou mapeie host.docker.internal:host-gateway em extra_hosts |
| Mensagem do Telegram nunca chega, sem erro | Timeout de resolução DNS IPv6 | NODE_OPTIONS=--dns-result-order=ipv4first no docker-compose |
new Date() retorna UTC apesar do cron em EST | Container sem a variável de ambiente TZ | Defina tanto TZ quanto GENERIC_TIMEZONE para seu fuso horário |
parse_mode: Markdown falha silenciosamente ou distorce | * ou _ sem par correspondente na saída da IA | Use parse_mode: HTML + sanitize antes de enviar |
| 401 no nó HTTP Request para LiteLLM | Cabeçalho de autenticação ausente | Adicione Authorization: Bearer {{ $env.LITELLM_API_KEY }} |
ECONNABORTED: timeout of 15000ms na chamada para Claude | Timeout padrão de 15s | Defina options.timeout: 90000 |
PUT para /api/v1/workflows/:id retorna 400 | Campos extras no corpo | Reduza para apenas: name, nodes, connections, settings, staticData |
| Renomear nó quebra o workflow silenciosamente | Referências de conexão usam o nome antigo | Substitua_tudo o nome antigo tanto em nodes[].name quanto nas chaves connections{} |
| Item pareado indisponível após agregação Code | N→1→M perdeu a linhagem do item | Defina executeOnce: true, itere $('UpstreamNode').all() explicitamente |
| Dedup com static data "não funciona" na execução manual | Execução manual reseta static data | Teste apenas via cron agendado |
Nó Code — as regras de modo/retorno
Todo nó Code tem um parâmetro mode. Incompatibilidade = bugs silenciosos.
| Modo | Padrão de acesso | Retorno |
|---|---|---|
runOnceForAllItems (padrão) | const items = $input.all(); | return items.map(...) — deve ser um array |
runOnceForEachItem | $json é o item atual | return { json: ... } — objeto único, NÃO array |
Sintomas de incompatibilidade:
runOnceForAllItems+ acessar$json→ apenas o primeiro item processado silenciosamenterunOnceForEachItem+ retornar array → erro de validação"A 'json' property isn't an object"
Padrão de dedup (estado entre execuções)
const store = $getWorkflowStaticData('global');
store.jobIds = store.jobIds || [];
const newItems = items.filter(item => !store.jobIds.includes(item.json.id));
// Persistir novos IDs, limitar aos últimos 500 para evitar crescimento indefinido
store.jobIds = [...store.jobIds, ...newItems.map(i => i.json.id)].slice(-500);
return newItems;
Crítico: staticData persiste apenas em execuções agendadas. A execução manual reseta. Verifique o dedup observando duas execuções agendadas consecutivas, não clicando em Executar duas vezes.
Limpeza de cache por execução
// No topo do primeiro nó Code — limpa caches transitórios, mas preserva o estado entre execuções
const store = $getWorkflowStaticData('global');
for (const k of Object.keys(store)) {
if (k.startsWith('cjob_') || k.startsWith('cprep_') || k === 'pendingJobs') {
delete store[k];
}
}
// NUNCA exclua jobIds — o dedup entre execuções depende disso
Padrão de observabilidade do pipeline
Defina contadores em cada nó Code principal. Leia na etapa de formatação. Anexe à mensagem.
// Estágio 1 — Construir Tickers
const store = $getWorkflowStaticData('global');
store.pipeline = { ticker_count: tickers.length, fixed_count: fixed.length };
// Estágio 2 — Calcular Sinais
store.pipeline = store.pipeline || {};
store.pipeline.signal_count = results.length;
// Final — Formatar mensagem
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);
Problema de item pareado + solução alternativa
Sintoma: Após um nó de agregação (N itens → 1 prompt → 1 resposta LLM → dividido em M itens), o downstream $('UpstreamNode').item.json lança Paired item data for item from node 'X' is unavailable.
Soluções alternativas (em ordem de preferência):
-
Defina
executeOnce: trueno nó Code downstream, itere explicitamente: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; -
Passe IDs através de static data; procure-os explicitamente downstream.
-
pairedItem.sourceOverwrite— frágil, último recurso.
Padrões do Telegram
Modo HTML — apenas 7 tags permitidas
<b>, <i>, <u>, <s>, <a>, <code>, <pre> — qualquer outra coisa lança 400 Bad Request: Unsupported start tag.
Defesa de três camadas para conteúdo gerado por IA
- Prompt: diga explicitamente à IA quais tags são permitidas
- Sanitizar: remova tags não suportadas no código antes de enviar
- Escapar: escape HTML do conteúdo do usuário/IA (
&→&,<→<,>→>) antes de incorporar
Suprimir rodapé de atribuição do n8n
"additionalFields": {
"parse_mode": "HTML",
"appendAttribution": false,
"disable_web_page_preview": true
}
Token em .env vs credencial
Prefira o padrão de variável de ambiente + HTTP Request em vez do nó Telegram embutido + credencial:
- Portátil entre migrações
- Mais fácil de rotacionar (
nano .env+ reiniciar container) - Sem risco de bloqueio por chave de criptografia do banco de dados
- Padrão:
POST https://api.telegram.org/bot{{ $env.BOT_TOKEN }}/sendMessage - Compensação: o token fica na URL da requisição, podendo aparecer em dados de execução, logs de proxy e mensagens de erro. Desative o salvamento de dados de execução para este workflow, ou use a credencial Telegram embutida se outras pessoas puderem ler seus logs.
Padrões OAuth do Gmail
| Problema | Resolução |
|---|---|
invalid_grant ... refresh token revoked | OAuth em modo "Testing" expira ~7 dias. Re-autentique via UI, ou publique o aplicativo em Produção. |
payload.headers vazio (De/Assunto em branco) | O padrão simple: true remove cabeçalhos. Defina options.simple: false. |
| Alertas de segurança / recibos ausentes | Não exclua -category:promotions -category:social -category:updates — o Gmail roteia 90% dos e-mails relevantes para lá. Use apenas is:unread newer_than:Nd. |
| Necessidade de marcar como lido em várias contas | Requer credenciais por conta. Uma credencial Gmail não pode operar nos IDs de mensagens de outra conta. |
Padrões da REST API (operações em lote)
Auth: Cabeçalho X-N8N-API-KEY: <key>.
| Endpoint | Observações |
|---|---|
GET /api/v1/workflows | Retorna todos (paginado). Filtre no lado do cliente. |
GET /api/v1/workflows/:id | JSON completo do workflow. |
PUT /api/v1/workflows/:id | Estrito: apenas name, nodes, connections, settings, staticData. O próprio settings tem uma lista de permissões (veja abaixo). Remova todo o resto de um ciclo GET-then-PUT. |
DELETE /api/v1/workflows/:id | Exclui de verdade (a UI só oferece Arquivar). Use para limpeza em massa. |
POST /api/v1/workflows/:id/activate | Mesmo que Publicar na UI v2.x. |
Lista de permissões do campo settings (corpo PUT)
executionOrder, timezone, saveExecutionProgress, saveDataErrorExecution, saveDataSuccessExecution, saveManualExecutions, callerPolicy, callerIds, errorWorkflow, executionTimeout
O tipo type do nó é imutável após a criação
Um PUT que altera node.type retorna 200, mas o tipo permanece o mesmo. Para converter (ex.: telegram → httpRequest): substitua o objeto do nó inteiro (novo UUID para id, mesmo name). As conexões são roteadas por nome, então a substituição preservando o nome é segura.
Especificidades da UI v2.x do n8n
| Antigo (v1.x) | Novo (v2.x) |
|---|---|
| Alternar Ativar | Publicar (botão verde, canto superior direito) / Cancelar publicação |
| Excluir workflow | Apenas Arquivar (use REST API para exclusão real) |
Nó HTTP Request — obrigatórios para chamadas LLM
{
"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 }
}
}
O container n8n não tem curl
Para testes HTTP dentro do container, use o módulo https embutido do Node via docker exec:
docker exec n8n node -e "const https=require('https'); /* request */"
Pré-voo SSH (antes de qualquer heredoc no VPS)
ssh -o ServerAliveInterval=30 -o ServerAliveCountMax=10 <user>@<host>
# Verifique o prompt: [<user>@<host> ...]$
# Só ENTÃO cole o heredoc
Colar um heredoc bash no PowerShell após um SSH drop = centenas de erros de análise.
Padrão de suíte de avaliação (somente lógica)
Testes com entrada simulada via recurso de Avaliações do n8n detectam regressões de lógica sem queimar créditos de API. Execute novamente ANTES e DEPOIS de qualquer alteração na lógica de filtro, listas de palavras-chave ou regras de validação.
Anti-padrões
| Anti-padrão | O que fazer em vez disso |
|---|---|
| "Execute de novo, veja se funciona" | Leia o log de execução do início ao fim primeiro |
| Corrigir um nó de cada vez em várias idas e vindas | Audite o arquivo completo → script de patch em lote → verifique em uma passada |
Usar parse_mode: Markdown para saída de IA | parse_mode: HTML + sanitizador |
| Codificar chaves de API / IDs de chat no JSON do workflow | Referencie {{ $env.KEY }}, armazene em <arquivo-secreto> (chmod 600) |
| Renomear nós sem atualizar as conexões | substitua_tudo no nome do nó tanto em nodes[].name quanto nas chaves connections{} |
| Execução manual para testar dedup | Aguarde o trigger agendado |
| Timeout padrão do httpRequest para chamadas LLM | Defina mínimo de 90000ms |
Misturar runOnceForAllItems e $json | Combine o modo com o padrão de acesso |