Skip to content

Latest commit

 

History

5 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

monitor-licitacoes-pncp

Automação em n8n que monitora licitações públicas no PNCP (Portal Nacional de Contratações Públicas), deduplica contra PostgreSQL, notifica no Telegram apenas o que é novo e registra a saúde da própria execução.

Fluxo exportado em JSON, banco versionado em migrações numeradas, ambiente reproduzível em Docker.

Fluxo completo


O que faz

Todo dia às 07:15 (BRT), para cada perfil de busca ativo no catálogo:

  1. Monta a consulta à API pública do PNCP a partir do catálogo (palavra-chave, UF, modalidade, valor mínimo, janela de dias)
  2. Chama o endpoint /v1/contratacoes/publicacao e classifica a resposta em quatro situações: sucesso, sem resultados, erro de parâmetro e indisponibilidade
  3. Filtra os resultados por palavra-chave e valor mínimo
  4. Grava no PostgreSQL com ON CONFLICT DO NOTHING. O banco é quem decide o que é novo
  5. Notifica no Telegram só as licitações que o banco aceitou, com órgão, município, valor formatado, prazo de encerramento e link direto para o edital
  6. Pagina até acabar, respeitando um teto de segurança
  7. Fecha o registro de execução com status, contagens e mensagem de erro

Notificações


Arquitetura

Schedule → Config → Start Run → Get Profiles → Loop (batch 1)
                                                   ↓
                                            Build Request
                                                   ↓
                                          Dev Mode? ──→ Load Fixture
                                                   ↓         ↓
                                            Fetch PNCP → Classify Response
                                                              ↓
                    ┌──────────────┬──────────────┬───────────┴──────┐
                   ok            empty        client_error         retry
                    ↓              ↓              ↓                  ↓
             Parse & Filter    (segue)      Log Failure →    Increment Attempt
                    ↓                       Notify API Failure       ↓
               Has Rows?                                  Attempts Exhausted?
                ↓      ↓                                       ↓         ↓
             true    false ──────────────┐            Log Failure →   Wait Backoff
                ↓                        │            Notify API Failure   ↓
       Upsert Procurement                │                            (reentrada)
                ↓                        │
         Aggregate New                   │
                ↓                        │
         Has New Items? ─ false ─────────┤
                ↓ true                   │
         Format Message                  │
                ↓                        │
        Notify New Items                 │
                ↓                        │
          Mark Notified ─────────────────┤
                                         ↓
                                  Has Next Page?
                                    ↓         ↓
                                 Next Page  Loop Profiles → Finish Run

Banco

Schema pncp, o domínio:

Objeto Papel
search_profiles catálogo que governa o comportamento: palavra-chave, UF, modalidade, valor mínimo, janela
procurements licitações coletadas; PK é control_number (numeroControlePNCP)

Schema ops, a operação:

Objeto Papel
workflow_runs log de auditoria de cada execução
v_workflow_health leitura da saúde, com fuso convertido e duração calculada

Migrações sql/001 a sql/006, todas idempotentes. A 006 separa papéis: o owner escreve, o bi_reader só lê.


Decisões técnicas

O catálogo governa o comportamento. pncp.search_profiles não é uma tabela de parâmetros: é o que define quantas chamadas serão feitas, com qual recorte e com que janela. Adicionar um monitoramento novo é um INSERT, não uma alteração no fluxo.

A checagem do que já foi visto mora no banco, e não no n8n. O ON CONFLICT (control_number) DO NOTHING com RETURNING devolve exatamente as linhas que eram novas. Não há comparação, cache ou lista de vistos dentro da automação.

Erro é classificado, não tratado em bloco. Um Switch separa 5xx e timeout (retry com espera crescente) de 4xx (falha imediata, sem retry) e de 204 (sucesso sem resultados). Tratar tudo como "deu erro" produziria laço infinito num caso e alarme falso no outro.

Dois mecanismos distintos de falha. Log Failure cobre a falha prevista, a API fora do ar, capturada pelo Switch. O workflow de erro separado cobre a falha imprevista, exceção em qualquer nó. Os dois registram em ops.workflow_runs e os dois notificam no Telegram, por caminhos diferentes: nenhuma falha fica silenciosa.

Modo de desenvolvimento embutido. Um nó Load Fixture reproduz a saída do nó HTTP a partir de uma resposta real salva em tests/fixtures/. O fluxo inteiro é construído e testado com a API indisponível, o que neste projeto não foi hipótese.

Identificadores em inglês, comentários e documentação em português. Exceto os parâmetros da URL (dataInicial, codigoModalidadeContratacao), que são da API.

Segredo fora do repositório. O chat ID do Telegram vem de variável de ambiente, não do JSON versionado.


Aprendizados

A API responde HTML quando falha. O 504 vem com content-type: text/html, e um cliente que assume JSON quebra no parse. O fluxo valida o cabeçalho antes de parsear. Evidência versionada em tests/fixtures/publicacao_504.html.

204 não é falha. Consulta válida sem resultados devolve 204 No Content, não 200 com lista vazia.

O gateway corta antes do cliente. As falhas se concentram em ~70 s, independentemente do timeout configurado. Timeout no cliente não resolve indisponibilidade no servidor.

Diagnóstico por eliminação vale ser registrado. A hipótese de que o filtro uf derrubava o endpoint foi testada com 8 chamadas em duas rodadas e refutada: todas falharam igualmente. O incidente completo está em docs/incidente-2026-09-02.md: 28 tentativas em 1h23, zero sucessos.

Volume descoberto tarde muda o desenho. 1.503 registros num único dia, para uma única modalidade, obrigaram a reduzir a janela de 3 dias para 1 e a manter o filtro de UF no servidor. Estimar volume antes de dimensionar a paginação teria evitado o retrabalho.

Um nó Filter descarta o item, e junto a informação de controle de fluxo que ele carregava. Quando não há resultados, o Parse & Filter emite um item sentinela que carrega _remainingPages. Um Filter descartava esse item, e com ele o sinal que fechava o ciclo do loop: a execução terminava sem chamar o Finish Run e a linha ficava presa em running. Onde a decisão precisa continuar viva nos dois desfechos, o nó certo é o IF, com saída para cada caso.

Nó de agregação com entrada vazia não devolve vazio. Devolve um invólucro vazio. Contar o comprimento da lista não é o mesmo que verificar se há conteúdo. O resultado foi notificação em branco até a condição passar a filtrar por chave.

O workflow de erro do n8n não dispara em execução manual. Só em execução de produção. Durante o desenvolvimento, a exceção aparece apenas no editor.

Codificação é do sistema, não do dado. No Windows, open() do Python lê em cp1252 e quebra em qualquer acento vindo de API brasileira.

Tirar segredo do repositório tem custo de configuração. O acesso a variáveis de ambiente vem bloqueado por padrão no n8n; sem N8N_BLOCK_ENV_ACCESS_IN_NODE=false, quem clonar importa o fluxo e quebra no primeiro nó. O custo está documentado abaixo.


Verificação

Os quatro caminhos do fluxo foram exercitados:

Caminho Evidência
Sucesso com novidade docs/telegram-notificacoes.png
Sucesso sem novidade (já visto) docs/workflow-dedup.png, docs/workflow-health-dedup.png
Falha prevista (API fora) docs/workflow-api-failure.png
Falha imprevista (exceção) docs/telegram-error.png

O resultado em números, com a primeira execução trazendo 8 novas e 8 notificadas e as duas seguintes com zero:

Saúde da execução

E a falha real registrada com status HTTP, perfil e URL:

HTTP 504 | TI no Paraná | https://pncp.gov.br/api/consulta/v1/contratacoes/
publicacao?dataInicial=20260902&dataFinal=20260903&...

Limitações conhecidas

  • O fluxo não roda sozinho. O n8n é local, em Docker. Rodar headless no GitHub Actions exigiria exportar credenciais descriptografadas e guardá-las como secret de CI, risco desproporcional ao ganho. A hospedagem permanente pede um servidor onde o n8n mantenha as credenciais criptografadas no próprio banco.
  • Execuções interrompidas deixam registro órfão. A linha em running só é fechada por Finish Run ou pelo workflow de erro; cancelamento ou reinício do container deixa o registro preso. A correção natural é a Start Run expirar execuções antigas, e ela não foi implementada.
  • Uma execução malsucedida leva ~17 minutos. Três tentativas por perfil, cada uma esperando o corte de 70 s do gateway mais o backoff. Com muitos perfis isso escala mal; faltaria um disjuntor que pule os demais perfis após confirmar indisponibilidade.
  • Perfis com mesma modalidade e UF baixam as mesmas páginas mais de uma vez. A evolução seria buscar por (modalidade, uf) distintos e aplicar todas as palavras-chave sobre o mesmo lote.
  • Busca por palavra-chave é includes simples, sem tolerância a acento ou radical: "licitação" não casa com "licitacao".
  • Só a modalidade 6 (Pregão Eletrônico) está no catálogo.
  • Valor revisado sobrescreve o anterior; não há histórico de revisão.
  • Sem testes automatizados sobre paginação e parsing.
  • A API do PNCP é instável. Durante o desenvolvimento, o endpoint principal ficou indisponível por vários dias seguidos. O fluxo trata, registra, avisa e segue, mas não coleta.

Como reproduzir

1. Banco

Crie um PostgreSQL (o projeto usa Neon, plano gratuito) e rode as migrações na ordem:

psql "$DATABASE_URL" -f sql/001_schema_pncp.sql
psql "$DATABASE_URL" -f sql/002_schema_ops.sql
psql "$DATABASE_URL" -f sql/003_views.sql
psql "$DATABASE_URL" -f sql/004_seed_search_profiles.sql
psql "$DATABASE_URL" -f sql/005_modality_name.sql
psql "$DATABASE_URL" -f sql/006_grants.sql

2. Variáveis de ambiente

cp .env.example .env
# preencha TELEGRAM_CHAT_ID

Para obter o chat ID: crie um bot com o @BotFather, mande qualquer mensagem a ele e abra https://api.telegram.org/bot<TOKEN>/getUpdates.

3. n8n

docker compose up -d

Acesse http://localhost:5678 e:

  1. Crie a credencial Postgres (SSL: Require) e a credencial Telegram
  2. Importe workflows/monitor-licitacoes-pncp-error.json
  3. Importe workflows/monitor-licitacoes-pncp.json
  4. Em Settings → Error Workflow, aponte para o workflow de erro
  5. Rode com modo_dev: true para testar contra o fixture; mude para false para consultar a API

O docker-compose.yml já define N8N_BLOCK_ENV_ACCESS_IN_NODE=false, necessário para o nó Config ler o chat ID do ambiente.


Estrutura

sql/             migrações numeradas e idempotentes
workflows/       os dois fluxos exportados em JSON
tests/fixtures/  respostas reais da API (sucesso e erro)
docs/            capturas e registro de incidente

Stack

n8n · PostgreSQL · Docker · API REST pública do PNCP · Telegram Bot API


📧 felipemansini@hotmail.com · 💼 LinkedIn · 📔 Portfólio no Notion

About

Automação em n8n que monitora licitações públicas no PNCP, deduplica no PostgreSQL e notifica no Telegram apenas o que é novo — com classificação de erro, backoff e log da própria execução.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages