No n8n, é natural evoluir um fluxo até ele virar uma pequena linha de produção: webhooks, integrações, filtros, validações, mensagens, atualização de CRM, criação de registros, cobrança, emissão de nota, abertura de ticket. Tudo isso parece um “workflow”. Mas, quando há efeito colateral, cada execução é uma decisão com impacto real.
Em produção, o mesmo passo pode rodar duas vezes por motivos legítimos: reentrega de webhook, timeout, falha parcial, fila que reprocessa, duplicidade na origem. Se o seu fluxo “tenta de novo” e o mundo muda de novo, o dano não é técnico. É operacional.
O desafio — o mundo reprocessa, mas o cliente não perdoa duplicidade
A duplicidade raramente aparece como erro 500. Ela aparece como ruído caro: dois agendamentos, dois e-mails de confirmação, duas cobranças, dois tickets, duas suspensões, duas concessões. E, quando isso acontece, quase nunca existe um log que seja suficiente para explicar de forma curta. Começa a arqueologia: “qual execução fez o quê”, “qual foi a primeira”, “qual foi a segunda”, “qual chegou na integração”.
O ponto é que “executar com sucesso” não é o mesmo que “executar uma única vez”. Em fluxos com efeito colateral, “uma vez” é um requisito de governança, não um detalhe de implementação.
O cenário — o mesmo evento chega duas vezes e o workflow faz o que foi desenhado para fazer
Um exemplo típico: o gateway manda um webhook. A primeira entrega chega, o fluxo processa, mas a resposta demora. O gateway reentrega. O segundo evento chega “igual”, e o n8n roda de novo. Se o fluxo cria um registro, dispara mensagem e atualiza um status, você acabou de duplicar o mundo.
Esse é o tipo de falha que a operação descobre tarde: pelo cliente, pelo suporte ou pela conciliação. E a correção vira trabalho humano distribuído, porque o sistema não foi desenhado para afirmar “isso eu já fiz”.
Implicações — idempotência é um portão antes do efeito colateral
Idempotência, aqui, não é filosofia. É um portão operacional: antes de executar uma ação com impacto, o fluxo precisa ter uma forma de decidir “este evento já foi consumido com sucesso?” e, se sim, não repetir.
O ponto mais importante é separar duas coisas: detecção e execução. A detecção precisa ser barata, consistente e rastreável. A execução pode falhar, pode ser reprocessada, pode sofrer instabilidade. Mas a decisão “faço ou não faço de novo” tem que ser estável.
A forma mais comum e prática é usar uma chave de idempotência persistida (ex.: Postgres) com garantia de unicidade. O objetivo não é “evitar todos os erros”; é evitar que um erro se multiplique.
-- Tabela simples de idempotência para ações com efeito colateralCREATE TABLE IF NOT EXISTS idempotency_keys (scope TEXT NOT NULL, -- ex.: "cobranca", "agendamento", "email"idempotency_key TEXT NOT NULL, -- chave derivada do evento (ex.: provider_event_id)first_seen_at TIMESTAMPTZ NOT NULL DEFAULT now(),status TEXT NOT NULL DEFAULT 'LOCKED', -- LOCKED | DONE | FAILEDmetadata JSONB NOT NULL DEFAULT '{}'::jsonb,PRIMARY KEY (scope, idempotency_key));-- Tentativa de "adquirir" o direito de executar (se já existir, não executa de novo)-- Em n8n, isso costuma ser um nó de Postgres antes do passo com efeito colateral.INSERT INTO idempotency_keys (scope, idempotency_key, metadata)VALUES ($1, $2, $3::jsonb)ON CONFLICT (scope, idempotency_key) DO NOTHING;
Antes do primeiro efeito colateral, o workflow tenta inserir a chave. Se inserir, o fluxo “ganhou o direito” de seguir. Se não inserir, alguém já processou aquilo — e o comportamento correto tende a ser terminar sem repetir ação.
// n8n "Code" node: derive uma Idempotency Key estável e carregue contexto mínimo.// Use antes do nó de Postgres que faz o INSERT ... ON CONFLICT DO NOTHING.const item = $input.first().json;// Ex.: prefira um ID do provedor (gateway/webhook) ou um identificador imutável do evento.const providerEventId = item.provider_event_id || item.eventId || item.id;// Escopo separa chaves por tipo de efeito colateral (evita colisão entre domínios).const scope = item.scope || "agendamento";// Se você não tem um ID imutável, é melhor hesitar do que inventar chave frágil.if (!providerEventId) {return [{json: {ok: false,reason: "missing_provider_event_id",scope,// carrega evidência mínima para revisão sem dados sensíveishint: "Sem ID imutável do evento, o fluxo deve pausar/escapar antes do efeito colateral."}}];}// Uma chave simples e rastreável (scope + ID do evento). Evite incluir timestamps.const idempotency_key = `${scope}:${providerEventId}`;return [{json: {ok: true,scope,idempotency_key,// contexto mínimo: o suficiente para correlação e auditoria do casocorrelation_id: item.correlation_id || providerEventId,metadata: {source: item.source || "webhook",received_at: new Date().toISOString()}}}];
Depois disso, o fluxo usa o resultado do INSERT para decidir: segue para o efeito colateral ou encerra como duplicado. E, ao final do caminho feliz, atualiza status = DONE para deixar claro que aquela chave não é só “vista”, mas “concluída” — evitando repetir ações que já foram efetivamente aplicadas.
O ganho operacional é imediato: você passa a ter um ponto único onde a duplicidade é bloqueada, e uma trilha curta para responder “foi executado?”, “quando?”, “com qual escopo?”. Isso reduz retrabalho, reduz contestação interna e diminui o dano quando o mundo reprocessa.
Síntese final — o n8n não precisa ser perfeito; precisa ser governável
Em automações com efeito colateral, duplicidade é uma falha de governança, não uma anomalia. O sistema pode ser robusto e ainda assim repetir ações. O que separa maturidade de fragilidade é existir um portão simples que impede “duas vezes” e que deixa rastro do porquê o fluxo seguiu ou parou.
Se você só descobre duplicidade pelo cliente, a operação já está pagando a conta do jeito mais caro: tarde, manual e politicamente.
O que ainda poderia melhorar — sinais de próxima maturidade
O próximo degrau aparece quando cada classe de ação tem seu escopo de idempotência explícito, quando o fluxo diferencia “vi” de “concluí” para não travar nem repetir, quando a correlação vira padrão e não exceção, quando duplicidade vira métrica e não surpresa, quando existe saída legítima para pausar na falta de ID imutável, e quando a organização consegue provar rapidamente por que um evento não gerou segunda ação sem depender de arqueologia em logs.