Code: Idempotência no n8n contra ações duplicadas

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 colateral
CREATE 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 | FAILED
  metadata         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íveis
      hint: "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 caso
    correlation_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.

Veja também: