Catalisa.
Building blocks/DecisãoBeta

Decision Platform

A esteira que busca os dados, aplica a política e devolve o veredito

26
Endpoints
5
Entidades
3
Provedores
Tenant
Escopo
3011
Porta

Uma chamada de API entra com o CPF e sai com a decisão. No meio, a esteira busca sozinha os dados que faltam — bureau, arquivo, fila — junta com o que você mandou e aplica a política vigente. Tudo registrado, com o tempo de cada etapa.

Para quem é
  • Fintechs de crédito que montam esteira de análise consultando fontes externas antes de decidir
  • Seguradoras que precisam enriquecer a proposta com dados de terceiros antes da aceitação
  • Times que hoje têm um serviço próprio só para orquestrar chamadas a bureau e regra
Substitui
  • Assinatura de uma plataforma de orquestração de decisão (Provenir, Taktile) para o caso de esteira única
  • O microserviço caseiro que só existe para chamar o bureau, esperar, juntar o JSON e chamar a regra
  • Código de retry, timeout e callback escrito à mão em volta de cada integração de dados
O que não é
  • Um motor de regras — ela não guarda nenhuma regra própria, chama o Decision Engine para isso
  • Um orquestrador de workflow com várias etapas de decisão encadeadas; cada config aponta para exatamente uma decisão
  • Um marketplace de dados; você traz suas próprias credenciais e endpoints de bureau
  • Um sistema de propostas ou de originação com interface e fila de esteira humana

01Resumo executivo

O Decision Platform é a esteira. Você faz uma chamada com o que tem em mãos — um CPF, um valor pedido —, e ela cuida do resto: busca os dados que faltam nas fontes que você declarou, junta tudo com o que você mandou, aplica a política de crédito vigente e devolve a decisão. Uma chamada entra, um veredito sai.

Na prática, ela substitui aquele serviço que quase toda fintech de crédito acaba escrevendo: o que só existe para chamar o bureau, esperar a resposta, tratar o timeout, juntar o JSON e então chamar a regra. Esse serviço nunca é o produto da empresa, mas consome time de engenharia todo mês e é onde os incidentes de esteira nascem. Aqui ele vira configuração: você declara as fontes, aponta para a política e chama POST /decision-platform/api/v1/execute.

A política em si não mora aqui. Ela mora no Decision Engine, como tabela DMN versionada. Essa separação é deliberada e é o assunto da §12.

Está em beta desde abril de 2026 — o modo síncrono está completo e o assíncrono ainda não processa (§15).

AtributoValor
Identificadordecision-platform
CategoriaDecisão
EscopoTenant (exige organizationId no token)
Porta (standalone)3011
Path alias@decision-platform
Prefixo HTTP/decision-platform
Schema no bancoplatform
StatusBeta desde 2026-04
Depende dePostgreSQL, Redis, S3, runner DMN, Decision Engine, IAM

02O problemanegócio

O cenário. Uma fintech de crédito recebe uma proposta com CPF e valor. Para decidir, ela precisa de coisas que não vieram na proposta: score de bureau, histórico interno, restrições de uma lista, limites da carteira. Só depois de reunir isso é que a política pode ser aplicada.

O que trava hoje.

  • A cola vira um serviço, e o serviço vira um problema. Buscar bureau, tratar timeout, tentar de novo, juntar o JSON e chamar a regra é código que nunca é o produto da empresa e que ninguém quer manter. Mesmo assim ele cresce a cada fonte nova.
  • Cada fonte de dados é um projeto. Adicionar um bureau significa código novo, deploy, tratamento de erro específico e uma nova forma de a esteira quebrar. Numa esteira com quatro fontes, são quatro maneiras diferentes de falhar.
  • Timeout é decisão de negócio tratada como detalhe técnico. Se o bureau está lento, a esteira deve esperar ou decidir sem ele? A resposta muda por fonte e por produto, mas costuma estar enterrada num axios.timeout que ninguém revisa.
  • Não dá para responder onde o tempo foi. "A análise está lenta" é uma reclamação sem endereço quando não se separa o tempo de buscar dado do tempo de aplicar a regra. Sem essa separação, otimizar é chute.
  • A alternativa de mercado custa caro e leva junto a regra. Plataformas como Provenir, Taktile e Alloy resolvem isso bem, mas nenhuma publica preço, todas vendem por cotação corporativa, e a política acaba escrita no formato proprietário delas — o que torna a saída cara depois.

O custo de não resolver. Some o custo direto — o time que mantém a cola, mês a mês, sem entregar produto — com o indireto: cada incidente de esteira é proposta parada, e proposta parada em originação digital é conversão perdida. E há o custo de não saber: sem o tempo de cada etapa medido, a discussão sobre latência da análise vira opinião, e a decisão de trocar de bureau é tomada sem dado.


03Proposta de valornegócio

AntesDepois
Um microserviço próprio só para orquestrar bureau e regraUma config com fontes declaradas e uma chamada de API
Fonte nova é código novo e deployFonte nova é POST .../data-sources
Timeout escondido no cliente HTTPtimeoutMs por fonte, com isRequired explícito
"A análise está lenta" sem endereçotiming com dataFetchMs e dmnExecutionMs separados
A política vive no formato do fornecedorA política é XML DMN padrão, no Decision Engine

A esteira é declarada, não programada. Cada fonte de dados é um registro com tipo, configuração, mapeamento de entrada, prioridade, obrigatoriedade e timeout próprio. Adicionar um bureau à esteira é uma chamada de API.

Fonte opcional que falha não derruba a decisão. Cada fonte tem isRequired. As obrigatórias que falham abortam a execução com erro explícito. As opcionais que falham simplesmente não contribuem, e a política decide com o que chegou — que é exatamente o comportamento que se quer quando um bureau secundário cai.

O tempo vem separado. Toda execução devolve timing com totalMs, dataFetchMs e dmnExecutionMs. Você sabe se a lentidão está no bureau ou na regra sem instrumentar nada.

Toda execução fica registrada. A tabela platform_executions guarda a entrada original, a entrada já enriquecida com as fontes, a saída, o erro, os tempos e um traceId seu para correlação. Reproduzir uma análise não depende de log de aplicação.

A regra continua portátil. A política é uma tabela DMN da OMG que vive no Decision Engine e sai de lá por API. Trocar de orquestrador não obriga a reescrever a política — o que não é verdade em plataforma de formato proprietário.


04Casos de uso reaisnegócio

Caso 1 — Uma esteira de crédito que enriquece a proposta antes de decidir Cenário ilustrativo

Contexto. Fintech de crédito pessoal com originação digital. A proposta chega com CPF, valor pedido e prazo. Para decidir, faltam score de bureau e histórico interno do cliente.

A dor. A cola era um serviço Node de uns 2 mil linhas: chamava o bureau, tratava timeout, tinha um retry que ninguém lembrava por que era três, montava o objeto e chamava a regra. Toda fonte nova mexia nesse serviço. Quando a análise ficava lenta, ninguém sabia dizer se era o bureau ou a regra, porque o log só media o total.

A solução com o BB. Uma DecisionPlatformConfig chamada analise-credito-pessoal aponta para a decisão politica-credito-pessoal do Decision Engine. Duas fontes são declaradas por POST /decision-platform/api/v1/configs/:configId/data-sources: uma HTTP para o endpoint do bureau, com isRequired: true, timeoutMs: 3000 e priority: 0; outra S3 para o arquivo diário de histórico interno, com isRequired: false e priority: 10. O inputMapping de cada uma extrai os campos por caminho — $.score, $.data.comprometimento — e os renomeia para os nomes que a tabela DMN espera. Publicar a config e a versão liga a esteira. O aplicativo chama POST /decision-platform/api/v1/execute com {"configKey":"analise-credito-pessoal","input":{"cpf":"...","valor":10000}}.

O resultado. O serviço de cola some. Fonte nova vira chamada de API. E timing.dataFetchMs contra timing.dmnExecutionMs responde, em toda execução, onde o tempo foi.

Caso 2 — Um bureau secundário cai e a esteira continua de pé Cenário ilustrativo

Contexto. A mesma esteira, agora com três fontes: bureau principal, bureau secundário de enriquecimento e arquivo interno.

A dor. Na versão caseira, qualquer fonte que passasse do timeout derrubava a análise inteira, porque o Promise.all não distinguia essencial de complementar. Uma instabilidade de vinte minutos no bureau secundário virava vinte minutos de propostas recusadas por erro técnico — o pior tipo de recusa, porque o cliente não volta.

A solução com o BB. O bureau principal fica isRequired: true. O secundário e o arquivo ficam isRequired: false. As fontes são buscadas em paralelo; quando uma opcional falha, o DataSourceService simplesmente não a inclui nos dados mesclados e segue. A tabela DMN é escrita já contando com isso: se o campo do bureau secundário não chegou, a linha que depende dele não casa e a decisão cai numa faixa mais conservadora — que é uma decisão de negócio, tomada na tabela, e não um erro técnico.

O resultado. A queda de uma fonte complementar vira uma decisão mais conservadora em vez de uma falha. E quem define o que é essencial é quem configura a esteira, não quem escreveu o cliente HTTP.

Caso 3 — Reconstruir uma análise seis meses depois Cenário ilustrativo

Contexto. Operação regulada precisa demonstrar como uma proposta específica foi analisada.

A dor. Reconstruir exigia juntar log de aplicação, log do bureau e o código da regra da época — três fontes com retenções diferentes, e nenhuma delas guardando o dado exato que a regra recebeu depois do enriquecimento.

A solução com o BB. GET /decision-platform/api/v1/executions?filter[configId]=...&filter[from]=... acha a execução. GET /decision-platform/api/v1/executions/:executionId devolve input (o que o chamador mandou), mergedInput (o que a regra de fato recebeu, já com os dados das fontes), output, timing e traceId. Do lado da regra, o versionId do Decision Engine leva ao XML DMN que rodou.

O resultado. A entrada enriquecida é o campo que faz a diferença: sem ele, saber o que o bureau respondeu naquele instante seria impossível. Com ele, a análise é reproduzível.

Caso 4 — Por que o mercado cobra caro por isso Referência de mercado

Contexto. Orquestração de decisão virou uma categoria própria e bem capitalizada. A Taktile anunciou uma Série C de US$ 110 milhões liderada pelo Goldman Sachs e lista Allianz, Monzo, Mercury e Kueski entre os clientes. A Provenir informa mais de 120 parceiros de dados, presença em mais de 60 países — com escritório em São Paulo — e mais de 4 bilhões de decisões processadas por ano. A Alloy informa mais de 900 instituições financeiras e mais de 270 integrações de dados.

A dor do mercado. Nenhuma dessas empresas publica preço. As três vendem por cotação, com ciclo de venda corporativo. Para uma fintech em estágio inicial, a orquestração de decisão fica economicamente inacessível na fase em que ela é mais necessária — e a saída é escrever a cola em casa, que é exatamente o problema do §2. (Todos os números acima são declarações públicas dos próprios fornecedores, consultadas em 2026-08-16.)

Como a Catalisa endereça. O Decision Platform entra como um building block dentro de uma plataforma que o cliente já contrata, e não como uma plataforma nova a integrar. E o ativo que mais custa migrar — a política — fica em XML DMN padrão no Decision Engine, exportável por API. O valor não está em ter mais integrações que a Provenir: não temos. Está em a esteira ser proporcional ao tamanho de quem começa, e em o custo de sair dela ser conhecido no dia da assinatura.


05Mercado e diferenciaisnegócio

Panorama. A orquestração de decisão de crédito se organiza em três grupos. As plataformas globais de decisão — Provenir, Taktile, Alloy — vendem a esteira completa com marketplace de dados, autoria visual e governança, por cotação corporativa. Os fornecedores brasileiros de dados e decisão — a Trillia, ex-Neurotech, hoje um negócio da B3, a Serasa Experian, a Equifax — vendem a decisão acoplada aos próprios dados, o que é forte no Brasil e cria dependência do mesmo grupo. E há os fornecedores de modelagem — Zest AI — que resolvem a parte estatística e não resolvem orquestração.

O Decision Platform não compete no tamanho do marketplace de dados: qualquer uma das plataformas globais tem mais integrações prontas do que nós, e isso não vai mudar. Ele compete em outro eixo — ser a esteira de quem já usa a plataforma Catalisa, com a política em formato padrão e o custo de saída conhecido.

CritérioCatalisa Decision PlatformProvenirTaktileTrillia (B3)Alloy
PreçoPrecificação em definiçãoNão públicoNão públicoNão públicoNão público
Integrações de dados prontasNenhuma — você traz o endpoint120+ parceirosMarketplace próprioEcossistema B3270+ soluções
Formato da políticaDMN (XML da OMG), portávelProprietárioProprietárioProprietárioProprietário
Autoria visual da esteiraNão (ver §15)SimSim, é a força delesSimSim
Execução assíncrona com callbackModelada, não processa hoje (§15)SimSimSimSim
Timing por etapa na respostaSim, dataFetch e dmn separadosNão documentado publicamenteNão documentado publicamenteNão documentado publicamenteNão documentado publicamente
Fonte opcional que falhaNão derruba a decisãoSimSimSimSim
Foco brasileiroPlataforma brasileiraEscritório em São PauloGlobalBrasileiro, é a força delesGlobal
Decisões encadeadas na esteiraNão — uma decisão por configSimSimSimSim

Nossos diferenciais

  1. A política não fica presa à esteira. A regra é um XML DMN da OMG no Decision Engine, extraível por API. Nas plataformas concorrentes o formato é próprio, e é isso que torna a migração cara — não a integração de dados, que se refaz. Esse diferencial é difícil de copiar porque contraria o modelo de retenção delas.
  2. O tempo de cada etapa vem na resposta. timing.dataFetchMs e timing.dmnExecutionMs separados, em toda execução, gravados no banco. É pouco glamouroso e é o que resolve a discussão sobre latência de esteira.
  3. Essencial e complementar é configuração, não código. isRequired e timeoutMs por fonte fazem a degradação graciosa ser uma decisão de quem monta a esteira, e não um efeito colateral de como o cliente HTTP foi escrito.
  4. A esteira já nasce dentro da plataforma. Ela usa o mesmo token, o mesmo organizationId e o mesmo vocabulário de permissões dos outros 31 building blocks, e conversa com Pricing Engine e Calculations Engine sem projeto de integração.

Quando escolher o concorrente. Se o que você precisa é não integrar bureau nenhum, a Provenir e a Alloy chegam com mais de cem integrações prontas e nós chegamos com zero — você traz o endpoint e a credencial, e isso é trabalho seu. Se o seu analista de risco precisa desenhar a esteira numa tela, arrastando etapas e testando cenários, a Taktile é hoje a melhor experiência de autoria do mercado e nós não temos interface. Se a sua operação depende profundamente de dados brasileiros — cadastro positivo, comportamento de mercado, modelos setoriais — a Trillia e a Serasa Experian vendem o dado e a decisão juntos, e essa integração vertical é uma vantagem real. Se você precisa de várias decisões encadeadas, com o resultado de uma alimentando a outra dentro da mesma esteira, nenhuma versão atual nossa faz isso: cada config aponta para exatamente uma decisão. E se você precisa de execução assíncrona em produção hoje, leia a §15 antes de qualquer coisa.


06Modelo de cobrança e ROInegócio

Unidade de cobrança. Precificação em definição. Quando definida, a unidade natural é a execução de esteira — uma decisão completa, do CPF ao veredito. É o que o cliente entende, e é o que a tabela platform_executions conta.

O que dispara custo.

DriverPor que ele importa
Execuções por mêsÉ a unidade de valor entregue
Fontes de dados por esteiraCada fonte é uma chamada de rede e um timeout a sustentar por execução
Retenção do históricoCada execução grava entrada, entrada enriquecida e saída em JSONB; auditoria longa custa armazenamento

Note que o custo do dado em si não é nosso. A consulta ao bureau é contratada por você, diretamente com o bureau. Isso é diferente do modelo das plataformas com marketplace, em que o dado é revendido junto — mais cômodo, e com uma margem embutida que você não enxerga.

Comparação de custo. Cenário: esteira com 3 fontes de dados e 100 mil análises por mês.

Catalisa Decision PlatformProvenirTaktileAlloy
Licença da plataformaPrecificação em definiçãoNão público — cotaçãoNão público — cotaçãoNão público — cotação
Custo do dado de bureauContratado por você, diretoPode vir pelo marketplacePode vir pelo marketplacePode vir pelo marketplace
Integração das 3 fontesConfiguração; sem códigoProvavelmente prontaProvavelmente prontaProvavelmente pronta
Migrar a política para foraBaixo — XML DMN por APIAlto — formato proprietárioAlto — formato proprietárioAlto — formato proprietário

Levantamento de 2026-08-16 nas páginas públicas dos fornecedores. Nenhum dos quatro concorrentes publica preço. Não estimamos valores que não conseguimos verificar — pedir cotação é o único caminho honesto. A linha "integração pronta" é inferência a partir do marketing público de cada um, não teste nosso.

ROI. A conta de guardanapo tem dois termos. O primeiro é o serviço de cola que deixa de existir: se hoje meio desenvolvedor por mês mantém o orquestrador caseiro, é esse custo, mais o custo dos incidentes que ele gera, que sai da conta. O segundo é o custo de saída, que quase nunca entra na planilha e deveria: numa plataforma de formato proprietário, migrar a política significa reescrevê-la e revalidá-la inteira — um projeto de meses num ativo que ninguém quer tocar. Aqui esse custo é baixar XMLs. Você não paga por isso hoje; você deixa de pagar depois.


07Arquitetura

                          HTTP  (Bearer JWT do IAM)
                                    │
  ┌─────────────────────────────────┴────────────────────────────────────────┐
  │ Hono app   basePath('/decision-platform')                                 │
  │                                                                           │
  │  /api/v1/configs   configsRouter       7 rotas   esteiras                 │
  │  /api/v1           versionsRouter      6 rotas   versões da esteira       │
  │  /api/v1           dataSourcesRouter   5 rotas   fontes de dados          │
  │  /api/v1           executionsRouter    4 rotas   execute + histórico      │
  │  /api/v1           orgConfigRouter     4 rotas   limites da organização   │
  │                                                                           │
  │  Toda rota: authMiddleware → requirePermission(P) → requireOrganization    │
  └─────────────────────────────────┬────────────────────────────────────────┘
                                    │
  ┌─────────────────────────────────┴────────────────────────────────────────┐
  │ ExecutionService                                                          │
  │   executeSync   → responde na hora                                        │
  │   executeAsync  → enfileira no Redis Stream e responde 202                │
  └───────┬──────────────────────────────────────────┬───────────────────────┘
          │                                          │
  ┌───────┴────────────────┐        ┌────────────────┴────────────────────────┐
  │ DataSourceService      │        │ AsyncExecutionConsumer                   │
  │   busca em PARALELO,   │        │   grupo 'execution-processors' no stream │
  │   ordena por priority  │        │   'decision-platform:executions'         │
  │   ┌──────────────────┐ │        │   ⚠ NÃO É INICIADO HOJE — ver §15        │
  │   │ S3Handler        │ │        └──────────────────┬───────────────────────┘
  │   │ RedisHandler     │ │                           │
  │   │ HttpHandler      │ │                    CallbackService
  │   └──────────────────┘ │                    (secureFetch, 3 tentativas)
  └───────┬────────────────┘
          │  mergedInput = { ...dadosDasFontes, ...inputDoChamador }
          ▼
  ┌───────────────────────────────────────────────────────────────────────────┐
  │ Decision Engine  (in-process)                                             │
  │   VersionRepository.findLatestPublished(decisionId)  → XML DMN            │
  │   DmnProxyService.execute(dmn, mergedInput)  ──HTTP──▶  runner DMN        │
  └───────────────────────────────────────────────────────────────────────────┘

A esteira ponta a ponta — o diagrama que descreve o produto:

  ┌──────────┐
  │ Proposta │  POST /decision-platform/api/v1/execute
  │  (CPF,   │  { "configKey": "analise-credito", "input": {...} }
  │  valor)  │
  └────┬─────┘
       │
       ▼
  ┌─────────────────────────────────────────────────────────────────────────┐
  │ 1. RESOLVE A ESTEIRA   config pela chave → versão publicada mais recente │
  └────┬────────────────────────────────────────────────────────────────────┘
       ▼
  ┌─────────────────────────────────────────────────────────────────────────┐
  │ 2. BUSCA OS DADOS   todas as fontes EM PARALELO, ordenadas por priority  │
  │                                                                          │
  │    ┌─────────────┐   ┌─────────────┐   ┌─────────────┐                   │
  │    │ HTTP        │   │ S3          │   │ REDIS_STREAM│                   │
  │    │ bureau      │   │ histórico   │   │ eventos     │                   │
  │    │ required ✓  │   │ opcional    │   │ opcional    │                   │
  │    │ 3000 ms     │   │ 5000 ms     │   │ 2000 ms     │                   │
  │    └──────┬──────┘   └──────┬──────┘   └──────┬──────┘                   │
  │           └─────────────────┴─────────────────┘                          │
  │                    inputMapping por fonte: $.score → score               │
  │           required que falha → 400 · opcional que falha → segue          │
  └────┬────────────────────────────────────────────────────────────────────┘
       ▼
  ┌─────────────────────────────────────────────────────────────────────────┐
  │ 3. JUNTA   mergedInput = { ...dadosDasFontes, ...inputDoChamador }       │
  │            o input do chamador SOBRESCREVE o das fontes em colisão       │
  └────┬────────────────────────────────────────────────────────────────────┘
       ▼
  ┌─────────────────────────────────────────────────────────────────────────┐
  │ 4. APLICA A POLÍTICA   Decision Engine → última versão PUBLISHED da      │
  │                        decisão apontada → runner DMN avalia a tabela     │
  └────┬────────────────────────────────────────────────────────────────────┘
       ▼
  ┌─────────────────────────────────────────────────────────────────────────┐
  │ 5. REGISTRA   PlatformExecution: input, mergedInput, output, traceId,    │
  │               timing { totalMs, dataFetchMs, dmnExecutionMs }            │
  └────┬────────────────────────────────────────────────────────────────────┘
       ▼
  ┌──────────────────┐          ┌──────────────────────────────────────────┐
  │  SYNC: 200 com   │          │  ASYNC: 202 na hora, callback depois     │
  │  a decisão       │          │  ⚠ o processador não roda hoje — §15     │
  └──────────────────┘          └──────────────────────────────────────────┘

  Onde entram precificação e cálculo: o Pricing Engine e o Calculations Engine
  NÃO são chamados pela esteira. Eles entram como fonte de dados do tipo HTTP,
  ou depois, quando o seu aplicativo já tem a decisão em mãos. Ver §12.

Decisões não óbvias.

  • O input do chamador vence o das fontes. A mesclagem é { ...mergedData, ...input.input }. Se o bureau devolveu score: 700 e o chamador mandou score: 800, a regra recebe 800. É deliberado: permite reprocessar uma proposta com valores fixados, para simular. E é uma superfície de abuso se o seu endpoint de execução for exposto a quem não deveria poder fixar o score — trate DECISIONS_EXECUTE como permissão sensível.
  • As fontes são buscadas em paralelo, e priority só ordena a mesclagem. priority não é ordem de execução — todas as chamadas saem juntas. Ele decide quem sobrescreve quem quando duas fontes trazem o mesmo campo: menor número vence menos, porque é mesclado primeiro. Fonte lenta não atrasa fonte rápida.
  • Fonte opcional que falha é silenciosa. O DataSourceService faz unwrapOr no resultado de cada fonte; a falha vira um resultado com success: false e dado vazio, e a mesclagem a ignora. A execução não guarda por fonte o que falhou. Isso mantém a esteira de pé e cobra o preço de a falha de fonte opcional não ser observável no registro da execução.
  • A versão da esteira congela a configuração, não a regra. O configSnapshot da PlatformConfigVersion guarda nome, chave, timeouts, callbackUrl e decisionId no momento da criação. Ele não guarda a versão do DMN. Na execução, o serviço lê o decisionId do snapshot e pede ao Decision Engine a última versão publicada daquela decisão. Consequência prática: publicar uma versão nova da política no Decision Engine muda o comportamento de esteiras já publicadas, na hora, sem passar por aqui. É intencional — a política é dela mesma, e não da esteira — e é a coisa mais importante desta seção.
  • As fontes de dados pertencem à config, não à versão. resolveDataSources busca as fontes pelo configId. Adicionar ou remover uma fonte afeta imediatamente todas as versões publicadas daquela esteira. Versionar não protege a lista de fontes.
  • A Platform alcança o Decision Engine no próprio processo, não por HTTP. Ela injeta o VersionRepository e o DmnProxyService do @decision-engine via container. Isso significa que o serviço decision-platform em standalone precisa de acesso ao schema decisions no banco e de DMN_ENGINE_URL próprio. Não há chamada HTTP ao serviço decision-engine: a variável MODULE_DECISION_ENGINE_URL presente no compose não é usada por estes caminhos. O ganho é uma ida de rede a menos por execução; o custo é acoplamento de banco entre os dois módulos.
  • O callback usa secureFetch com proteção contra SSRF. A callbackUrl é fornecida pelo cliente e aponta para fora. O CallbackService reusa o secureFetch do Webhooks Engine, que bloqueia destino interno. São até 3 tentativas, com esperas de 1 s, 5 s e 15 s, e retentativa apenas em erro de rede ou HTTP 5xx.
  • As fontes S3 e Redis são obrigadas ao prefixo da organização. O S3DataSourceHandler exige que a chave comece com {organizationId}/ ou shared/; o RedisDataSourceHandler exige que o nome do stream comece com {organizationId}: ou shared:. Não é convenção: é validação que recusa a configuração.

Monolito vs. standalone. Em monolito, tudo resolve pelo container TypeDI. Em standalone o serviço sobe na porta 3011 e continua precisando de PostgreSQL (schemas platform e decisions), Redis, S3 e DMN_ENGINE_URL. Em nenhum dos dois modos existe hoje um processo que consuma a fila assíncrona (§15).


08Conceitos e modelo de dados

Glossário

TermoSignifica
Config (esteira)A DecisionPlatformConfig. Define qual decisão aplicar, timeouts e callback. Tem chave estável (analise-credito).
Versão da esteiraPlatformConfigVersion. Congela a configuração em semver. Não congela a versão da regra.
configSnapshotO JSONB dentro da versão com nome, chave, timeouts, callbackUrl e decisionId no instante da criação.
Data source (fonte)Uma origem de dados da esteira: S3, REDIS_STREAM ou HTTP. Pertence à config.
inputMappingComo extrair da resposta da fonte ($.data.score) e para onde levar (score), com transformação opcional.
isRequiredSe a falha dessa fonte aborta a execução (true) ou é ignorada (false).
priorityOrdem de mesclagem, não de execução. Menor é mesclado primeiro, e portanto pode ser sobrescrito.
mergedInputO que a regra de fato recebeu: dados das fontes mesclados com o input do chamador.
ExecutionO registro de uma execução, com entrada, entrada enriquecida, saída, tempos e status.
traceIdCorrelação que você fornece. A plataforma só guarda e indexa.
Org configLimites por organização: concorrência, retentativas e retenção. Hoje armazenados, não aplicados (§15).

Modelo de dados — schema platform no PostgreSQL.

Modelo PrismaTabelaPropósitoCampos-chave
DecisionPlatformConfigplatform.decision_platform_configsA esteirakey único por organização, decisionId, status, defaultTimeoutMs, maxTimeoutMs, callbackUrl, callbackHeaders
PlatformConfigVersionplatform.platform_config_versionsVersão da esteiraversion único por config, status, configSnapshot (JSONB), publishedAt
PlatformDataSourceplatform.platform_data_sourcesFonte de dadosname único por config, type, typeConfig (JSONB), inputMapping (JSONB), priority, isRequired, timeoutMs
PlatformExecutionplatform.platform_executionsExecuçãoinput, mergedInput, output, error, executionTimeMs, dataFetchTimeMs, dmnExecutionMs, callbackAttempts, traceId
OrganizationPlatformConfigplatform.organization_platform_configsLimites da organizaçãomaxConcurrentExecutions, maxRetries, retryBackoffMs, retryBackoffMultiplier, executionRetentionDays

Enumerações

EnumValores
PlatformConfigStatusDRAFT · PUBLISHED · ARCHIVED
DataSourceTypeS3 · REDIS_STREAM · HTTP
PlatformExecutionModeSYNC · ASYNC
PlatformExecutionStatusPENDING · RUNNING · COMPLETED · FAILED · TIMEOUT · CANCELLED

Tipos de fonte e o que cada uma aceita

TipoConfiguração (typeConfig)Restrição de isolamento
HTTPurl, method (GET/POST), headers, body, paginationsecureFetch bloqueia destino interno (anti-SSRF)
S3bucket, key, format (JSON/PARQUET), regionkey precisa começar com {organizationId}/ ou shared/
REDIS_STREAMstreamName, consumerGroup, consumerName, count, startIdstreamName precisa começar com {organizationId}: ou shared:

PARQUET está no schema e não está implementado — a leitura retorna erro de validação (§15).

Transformações do inputMapping

transformO que faz
NONEUsa o valor como veio (padrão)
FLATTENAchata arrays aninhados
FIRSTPega o primeiro elemento do array
LASTPega o último elemento do array

O path é um subconjunto de JSONPath: $.campo, $.campo.aninhado, $.lista[0], $.lista[*]. Caminho que não começa com $. devolve undefined e cai no defaultValue, silenciosamente.

Máquinas de estado

  Config e Versão                        Execução
  ───────────────                        ────────

  ┌───────┐  POST /configs/:id/publish    ┌─────────┐
  │ DRAFT │ ─────────────────────────▶    │ PENDING │  criada (só em ASYNC)
  └───────┘                               └────┬────┘
      │  A versão tem o MESMO ciclo, com       │  o processador pega
      │  POST /versions/:id/publish            ▼
      ▼                                   ┌─────────┐
  ┌───────────┐                           │ RUNNING │
  │ PUBLISHED │  executa                  └────┬────┘
  └─────┬─────┘                                │
        │ POST /versions/:id/archive     ┌─────┴────────┬──────────┐
        ▼                                ▼              ▼          ▼
  ┌──────────┐                    ┌───────────┐  ┌────────┐  ┌─────────┐
  │ ARCHIVED │                    │ COMPLETED │  │ FAILED │  │ TIMEOUT │
  └──────────┘                    └───────────┘  └────────┘  └─────────┘

  Para uma esteira executar, PRECISA de pelo menos uma PlatformConfigVersion
  PUBLISHED. Sem ela: 400 "No published version found for this config".
  Publicar a CONFIG não basta — são dois objetos e dois publishes distintos.

  TIMEOUT e CANCELLED existem no enum e nenhum caminho de código os atribui.
  Em SYNC a execução vai direto de RUNNING para COMPLETED ou FAILED.

09Referência da API

Prefixo: /decision-platform. Em staging, a base é https://decision-platform.bb.stg.catalisa.app.

Todas as rotas exigem: authMiddleware (Bearer JWT do IAM), o requirePermission indicado, e requireOrganization — token sem organizationId recebe 403.

Corpos aceitam JSON:API ({"data":{"attributes":{...}}}) ou o objeto direto, exceto POST /execute, que lê o corpo cru.

Esteiras (configs) — /decision-platform/api/v1/configs

MétodoRotaDescriçãoPermissão
POST/decision-platform/api/v1/configsCria esteira em DRAFTDECISION_VERSIONS_CREATE
GET/decision-platform/api/v1/configsLista esteiras, paginadoDECISION_VERSIONS_READ
GET/decision-platform/api/v1/configs/:configIdBusca esteiraDECISION_VERSIONS_READ
GET/decision-platform/api/v1/configs/:configId/fullEsteira com versões e fontesDECISION_VERSIONS_READ
PATCH/decision-platform/api/v1/configs/:configIdAtualiza nome, timeouts e callbackDECISION_VERSIONS_CREATE
POST/decision-platform/api/v1/configs/:configId/publishPublica a esteiraDECISION_VERSIONS_CREATE
DELETE/decision-platform/api/v1/configs/:configIdExclusão lógica. Responde 204DECISION_PROJECTS_DELETE

Filtros de listagem: filter[status] (DRAFT, PUBLISHED, ARCHIVED), filter[decisionId] (UUID).

Repare que publicar a esteira exige DECISION_VERSIONS_CREATE e não DECISION_VERSIONS_PUBLISH, e que excluir exige DECISION_PROJECTS_DELETE. Estão transcritas do código. Conceda papéis por esta tabela, não pelo nome que a permissão sugere.

Versões da esteira — /decision-platform/api/v1

MétodoRotaDescriçãoPermissão
POST/decision-platform/api/v1/configs/:configId/versionsCria versão, congelando o configSnapshotDECISION_VERSIONS_CREATE
GET/decision-platform/api/v1/configs/:configId/versionsLista versões da esteiraDECISION_VERSIONS_READ
GET/decision-platform/api/v1/configs/:configId/versions/latestÚltima versão publicadaDECISION_VERSIONS_READ
GET/decision-platform/api/v1/versions/:versionIdBusca versãoDECISION_VERSIONS_READ
POST/decision-platform/api/v1/versions/:versionId/publishPublica a versãoDECISION_VERSIONS_PUBLISH
POST/decision-platform/api/v1/versions/:versionId/archiveArquiva a versãoDECISION_VERSIONS_CREATE

Filtro: filter[status]. Só versão PUBLISHED pode ser arquivada — arquivar DRAFT retorna 400.

Fontes de dados — /decision-platform/api/v1

MétodoRotaDescriçãoPermissão
POST/decision-platform/api/v1/configs/:configId/data-sourcesCria fonte na esteiraDECISION_VERSIONS_CREATE
GET/decision-platform/api/v1/configs/:configId/data-sourcesLista fontes da esteiraDECISION_VERSIONS_READ
GET/decision-platform/api/v1/data-sources/:dataSourceIdBusca fonteDECISION_VERSIONS_READ
PATCH/decision-platform/api/v1/data-sources/:dataSourceIdAtualiza fonteDECISION_VERSIONS_UPDATE
DELETE/decision-platform/api/v1/data-sources/:dataSourceIdExclusão lógica. Responde 204DECISION_VERSIONS_DELETE

Filtro de listagem: filter[type] (S3, REDIS_STREAM, HTTP).

Execução — /decision-platform/api/v1

MétodoRotaDescriçãoPermissão
POST/decision-platform/api/v1/executeExecuta a esteira. 200 em SYNC, 202 em ASYNCDECISIONS_EXECUTE
GET/decision-platform/api/v1/executionsLista execuções, paginadoDECISIONS_READ
GET/decision-platform/api/v1/executions/:executionIdExecução completa, com mergedInputDECISIONS_READ
GET/decision-platform/api/v1/executions/:executionId/statusSó status, datas e erroDECISIONS_READ

Filtros de listagem: filter[configId], filter[versionId] (UUID), filter[status], filter[mode], filter[from], filter[to] (ISO-8601).

Limites da organização — /decision-platform/api/v1

MétodoRotaDescriçãoPermissão
GET/decision-platform/api/v1/org-configLimites atuais. Devolve os padrões com isDefault: true se nunca configuradoDECISIONS_READ
PUT/decision-platform/api/v1/org-configSubstitui os limitesDECISIONS_MANAGE
PATCH/decision-platform/api/v1/org-configAtualiza campos avulsosDECISIONS_MANAGE
DELETE/decision-platform/api/v1/org-configVolta aos padrões. Responde 204DECISIONS_MANAGE

Estes valores são persistidos e devolvidos, mas nenhum deles é aplicado hoje pelo motor de execução. Ver §15 antes de contar com eles.

Saúde

MétodoRotaDescrição
GET/decision-platform/healthSonda de vida do processo, com versão do build. Pública.

POST /decision-platform/api/v1/execute

O endpoint que importa. Resolve a esteira pela chave, busca os dados, aplica a política e devolve.

Request

{
  "configKey": "analise-credito-pessoal",
  "mode": "SYNC",
  "input": { "cpf": "12345678901", "valorSolicitado": 10000 },
  "traceId": "proposta-2026-08-16-00042",
  "timeoutMs": 15000
}
CampoTipoObrigatórioDescrição
configKeystring (1–100)SimChave da esteira. Não é o UUID.
modeSYNC | ASYNCNãoPadrão SYNC. Sobre ASYNC, leia §15
inputobjectNãoPadrão {}. Mesclado por cima dos dados das fontes
versionIdstring (UUID)NãoFixa uma versão da esteira. Precisa estar PUBLISHED e ser desta config
callbackUrlstring (URL)NãoSobrepõe o callback da config. Só usado em ASYNC
callbackHeadersobjectNãoCabeçalhos extras do callback
timeoutMsint 1000–300000NãoSobrepõe o defaultTimeoutMs da config
traceIdstring (≤255)NãoSua correlação. Indexado

Resposta 200 — modo SYNC

{
  "data": {
    "type": "platform-execution",
    "id": "9c2f...",
    "attributes": {
      "status": "COMPLETED",
      "mode": "SYNC",
      "output": { "decisao": "APROVADO", "limite": 8000 },
      "timing": { "totalMs": 412, "dataFetchMs": 366, "dmnExecutionMs": 41 }
    }
  },
  "links": { "self": "/api/v1/executions/9c2f..." }
}

O timing é o produto em três números: quanto tempo total, quanto foi buscar dado, quanto foi avaliar a regra.

Resposta 202 — modo ASYNC

{
  "data": {
    "type": "platform-execution",
    "id": "9c2f...",
    "attributes": { "status": "PENDING", "mode": "ASYNC", "callbackUrl": "https://...",
                    "traceId": "proposta-2026-08-16-00042" }
  },
  "links": { "self": "/api/v1/executions/9c2f..." }
}

Aviso. A execução é criada e enfileirada no Redis Stream, mas nenhum processo consome essa fila na configuração atual. A execução permanece PENDING indefinidamente e o callback não é entregue. Use SYNC em produção até que §15 registre a mudança.

Erros

StatusQuando
400No published version found for this config — a esteira não tem versão publicada
400Version must be published to execute — o versionId informado não está PUBLISHED
400Required data source '<nome>' failed: ... — uma fonte obrigatória não respondeu
400No decision linked to this platform config — o configSnapshot não tem decisionId
400No published decision version found for decision — a decisão no Decision Engine não tem versão publicada
400DMN execution failed: ... — o runner recusou o XML ou a entrada mesclada
403Token sem organizationId ou sem DECISIONS_EXECUTE
404Platform config — não existe esteira com essa chave nesta organização

Quando a execução falha depois de criada, a linha em platform_executions fica com status: FAILED e a mensagem em error. O executionId do erro não é devolvido no corpo — encontre a execução por filter[traceId] ou pelo intervalo de tempo.


POST /decision-platform/api/v1/configs/:configId/data-sources

Declara uma fonte de dados na esteira. Exemplo de um bureau por HTTP.

Request

{
  "name": "bureau-principal",
  "type": "HTTP",
  "typeConfig": {
    "url": "https://api.bureau-exemplo.com.br/v1/score",
    "method": "POST",
    "headers": { "Authorization": "Bearer ..." },
    "body": { "documento": "PLACEHOLDER" }
  },
  "inputMapping": [
    { "path": "$.score", "targetField": "score", "transform": "NONE" },
    { "path": "$.restricoes[*]", "targetField": "restricoes", "transform": "FLATTEN" },
    { "path": "$.renda.estimada", "targetField": "rendaEstimada", "defaultValue": 0 }
  ],
  "priority": 0,
  "isRequired": true,
  "timeoutMs": 3000
}
CampoTipoObrigatórioDescrição
namestring (1–100)SimÚnico dentro da esteira
typeS3 | REDIS_STREAM | HTTPSimDetermina o schema de typeConfig
typeConfigobjectSimValidado conforme o tipo; erro traz o motivo
inputMappingarray (mín. 1)Simpath, targetField, transform, defaultValue
priorityint 0–100NãoPadrão 0. Ordem de mesclagem, não de execução
isRequiredbooleanNãoPadrão false
timeoutMsint 1000–300000NãoPadrão 5000

Erros

StatusQuando
400Invalid typeConfig for <TIPO>: ... — a configuração não bate com o tipo
409Já existe fonte com esse nome nesta esteira

A typeConfig é gravada como JSONB sem criptografia. Não coloque segredo de longa duração em headers — ver §14.


GET /decision-platform/api/v1/executions/:executionId

Devolve a execução completa. O campo que justifica a existência do endpoint é o mergedInput.

{
  "data": {
    "type": "platform-execution",
    "id": "9c2f...",
    "attributes": {
      "configId": "...", "versionId": "...", "mode": "SYNC", "status": "COMPLETED",
      "input":       { "cpf": "12345678901", "valorSolicitado": 10000 },
      "mergedInput": { "score": 730, "restricoes": [], "rendaEstimada": 5200,
                       "cpf": "12345678901", "valorSolicitado": 10000 },
      "output": { "decisao": "APROVADO", "limite": 8000 },
      "error": null,
      "timing": { "totalMs": 412, "dataFetchMs": 366, "dmnExecutionMs": 41 },
      "callbackAttempts": 0, "traceId": "proposta-2026-08-16-00042",
      "executedBy": "...", "startedAt": "...", "completedAt": "..."
    }
  }
}

input é o que o chamador mandou; mergedInput é o que a regra recebeu. A diferença entre os dois é o que as fontes trouxeram.


10Início rápido

Uma esteira ponta a ponta, do zero à primeira decisão. Assume que você já criou a decisão no Decision Engine — se ainda não, siga primeiro o início rápido de lá.

1. Autenticar

TOKEN=$(curl -s -X POST https://iam.bb.stg.catalisa.app/iam/api/v1/users/login \
  -H "Content-Type: application/json" \
  -d '{
    "email": "admin@test.com",
    "password": "password123",
    "organizationId": "b0000000-0000-0000-0000-000000000001"
  }' | jq -r .accessToken)

BASE=https://decision-platform.bb.stg.catalisa.app/decision-platform/api/v1
ENGINE=https://decision-engine.bb.stg.catalisa.app/decision-engine/api/v1

2. Descobrir a decisão que a esteira vai aplicar

DECISION_ID=$(curl -s "$ENGINE/decisions" -H "Authorization: Bearer $TOKEN" \
  | jq -r '.data[] | select(.attributes.key=="checagem-idade") | .id')

Ela precisa estar ACTIVE e ter uma versão PUBLISHED, senão a esteira falha no passo 7.

3. Criar a esteira

CONFIG_ID=$(curl -s -X POST "$BASE/configs" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d "{
    \"decisionId\": \"$DECISION_ID\",
    \"name\": \"Análise simples\",
    \"key\": \"analise-simples\",
    \"defaultTimeoutMs\": 15000,
    \"maxTimeoutMs\": 30000
  }" | jq -r '.data.id')

4. Declarar uma fonte de dados (opcional — a esteira funciona sem nenhuma)

curl -s -X POST "$BASE/configs/$CONFIG_ID/data-sources" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{
    "name": "idade-externa",
    "type": "HTTP",
    "typeConfig": { "url": "https://api.exemplo.com.br/perfil", "method": "GET" },
    "inputMapping": [ { "path": "$.idade", "targetField": "age", "defaultValue": 0 } ],
    "priority": 0,
    "isRequired": false,
    "timeoutMs": 3000
  }' | jq '.data.attributes.name'

O targetField é age porque é assim que a tabela DMN chama a variável. O mapeamento é a cola entre a fonte e a regra.

5. Criar e publicar a versão da esteira

VERSION_ID=$(curl -s -X POST "$BASE/configs/$CONFIG_ID/versions" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"version":"1.0.0"}' | jq -r '.data.id')

curl -s -X POST "$BASE/versions/$VERSION_ID/publish" \
  -H "Authorization: Bearer $TOKEN" | jq '.data.attributes.status'
# "PUBLISHED"

6. Publicar a esteira

curl -s -X POST "$BASE/configs/$CONFIG_ID/publish" \
  -H "Authorization: Bearer $TOKEN" | jq '.data.attributes.status'
# "PUBLISHED"

São dois objetos e dois publishes: a config e a versão. O que a execução exige é a versão publicada.

7. Executar a esteira

curl -s -X POST "$BASE/execute" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{
    "configKey": "analise-simples",
    "mode": "SYNC",
    "input": { "age": 25 },
    "traceId": "teste-inicio-rapido-1"
  }' | jq
{
  "data": {
    "type": "platform-execution",
    "id": "9c2f...",
    "attributes": {
      "status": "COMPLETED",
      "mode": "SYNC",
      "output": { "result": "adult" },
      "timing": { "totalMs": 68, "dataFetchMs": 21, "dmnExecutionMs": 39 }
    }
  }
}

8. Ver o que a regra realmente recebeu

curl -s "$BASE/executions" -H "Authorization: Bearer $TOKEN" \
  | jq '.data[0].attributes | {input, mergedInput, timing}'

Credenciais de staging, conforme AMBIENTES.md. Nunca use credencial de produção em documentação ou script de exemplo.


11Receitas

Montar uma esteira de crédito com bureau obrigatório e enriquecimento opcional

O padrão de esteira mais comum: uma fonte que não pode faltar e outras que são bônus.

# Bureau principal — sem ele não há decisão
curl -s -X POST "$BASE/configs/$CONFIG_ID/data-sources" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{
    "name":"bureau-principal","type":"HTTP",
    "typeConfig":{"url":"https://api.bureau.com.br/score","method":"POST",
                  "headers":{"Authorization":"Bearer TOKEN_DO_BUREAU"},
                  "body":{"documento":"placeholder"}},
    "inputMapping":[{"path":"$.score","targetField":"score"},
                    {"path":"$.restricoes[*]","targetField":"restricoes","transform":"FLATTEN"}],
    "priority":0,"isRequired":true,"timeoutMs":3000
  }'

# Histórico interno em S3 — se faltar, a política decide mais conservadora
curl -s -X POST "$BASE/configs/$CONFIG_ID/data-sources" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{
    "name":"historico-interno","type":"S3",
    "typeConfig":{"bucket":"catalisa-dados",
                  "key":"b0000000-0000-0000-0000-000000000001/historico/2026-08-16.json",
                  "format":"JSON"},
    "inputMapping":[{"path":"$.comprometimento","targetField":"comprometimento","defaultValue":0}],
    "priority":10,"isRequired":false,"timeoutMs":5000
  }'

Armadilhas.

  • A chave do S3 precisa começar com o organizationId ou com shared/. Qualquer outro prefixo é recusado na validação, com mensagem explícita. O mesmo vale para o nome do stream Redis, com {organizationId}: ou shared:.
  • A fonte opcional que falha some sem deixar rastro na execução. Escreva a tabela DMN com defaultValue e uma linha que trate a ausência do campo — não presuma que ele sempre chega.
  • priority não é ordem de execução. Todas as fontes saem em paralelo. Ele só decide quem sobrescreve quem na mesclagem, e a fonte com priority maior é mesclada depois, prevalecendo.
  • Adicionar uma fonte afeta imediatamente todas as versões publicadas da esteira. As fontes pertencem à config, não à versão. Não existe versionamento da lista de fontes.

Trocar a política sem tocar na esteira

O objetivo é mudar a regra de crédito sem mexer em nenhuma configuração daqui.

# Tudo acontece no Decision Engine
VERSION_ID=$(jq -Rs '{version:"1.1.0", content:.}' < politica-nova.dmn \
  | curl -s -X POST "$ENGINE/decisions/$DECISION_ID/versions" \
      -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d @- \
  | jq -r '.data.id')

curl -s -X POST "$ENGINE/versions/$VERSION_ID/publish" -H "Authorization: Bearer $TOKEN"

# A esteira já está usando a política nova. Nenhuma chamada ao decision-platform.

Armadilhas. Esta é a coisa mais importante do building block, e ela corta dos dois lados. A esteira sempre usa a última versão publicada da decisão — publicar no Decision Engine muda o comportamento de todas as esteiras que apontam para ela, na hora. Uma versão publicada da esteira não protege contra isso, porque o configSnapshot congela o decisionId, e não a versão do DMN. Se você precisa de duas esteiras com políticas diferentes, use duas decisões distintas no Decision Engine, não duas versões da mesma.

Reconstruir uma análise para auditoria

# 1. Achar pela sua correlação ou pelo intervalo
curl -s "$BASE/executions?filter[configId]=$CONFIG_ID&filter[from]=2026-08-01" \
  -H "Authorization: Bearer $TOKEN" | jq '.data[] | {id, traceId: .attributes.traceId}'

# 2. A execução completa, com o que a regra recebeu
curl -s "$BASE/executions/$EXEC_ID" -H "Authorization: Bearer $TOKEN" \
  | jq '.data.attributes | {input, mergedInput, output, timing, versionId}'

# 3. A REGRA que rodou, do lado do Decision Engine
curl -s "$ENGINE/decisions/$DECISION_ID/execution-logs?filter[from]=..." \
  -H "Authorization: Bearer $TOKEN" | jq '.data[0].attributes.versionId'

Armadilhas. O versionId que a PlatformExecution guarda é o da versão da esteira, não o da versão do DMN. Para saber qual tabela rodou, é preciso cruzar pelo horário com o ExecutionLog do Decision Engine — não há ligação direta entre as duas execuções (§15). Guarde o mesmo traceId no seu lado, é o que torna esse cruzamento suportável.

Diagnosticar uma esteira lenta

curl -s "$BASE/executions?filter[status]=COMPLETED&page[size]=100" \
  -H "Authorization: Bearer $TOKEN" \
  | jq '[.data[].attributes.timing] | {
      total_medio:     (map(.totalMs)         | add / length),
      busca_media:     (map(.dataFetchMs)     | add / length),
      regra_media:     (map(.dmnExecutionMs)  | add / length)
    }'

Armadilhas. dataFetchMs é o tempo da fonte mais lenta, porque a busca é paralela — não é a soma. Se ele está alto, uma fonte está segurando todas: reduza o timeoutMs dela e a marque como opcional, e a esteira passa a degradar em vez de esperar. Se dmnExecutionMs está alto, o problema é o tamanho da tabela DMN ou a latência até o runner, e a investigação continua no Decision Engine.

Preparar-se para o modo assíncrono

Enquanto o processador não roda (§15), a alternativa em produção é assumir a assincronia do seu lado.

# Chame SYNC a partir da sua própria fila de trabalho, com o traceId da proposta
curl -s -X POST "$BASE/execute" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d "{\"configKey\":\"analise-credito\",\"mode\":\"SYNC\",
       \"input\":{...},\"traceId\":\"$PROPOSTA_ID\",\"timeoutMs\":20000}"

Armadilhas. Dimensione o timeoutMs acima da soma esperada: ele precisa cobrir a fonte mais lenta mais a avaliação da regra. O teto é 300000 ms, mas manter uma conexão HTTP aberta por minutos é frágil — o caminho saudável é apertar o timeout das fontes. E não use mode: "ASYNC" esperando callback: a chamada responde 202 e a execução fica PENDING para sempre.


12Integração com outros building blocks

A fronteira que mais gera dúvida: Decision Platform ou Decision Engine?

Esta é a primeira pergunta de todo integrador, e a resposta é curta: o Decision Engine é a regra; o Decision Platform é a esteira que leva os dados até a regra.

Decision EngineDecision Platform
O que ele guardaA regra: XML DMN versionadoA esteira: quais fontes, timeouts, callback
Quem escreveAnalista de risco, em tabela DMNEngenharia, em configuração
De onde vêm os dadosDo chamador, no corpo da requisiçãoDas fontes declaradas, mais o corpo
Chamada típicaPOST /decisions/:id/executePOST /execute com configKey
Tem regras próprias?Sim, é a razão de existirNão. Sempre chama o Engine
Modo assíncronoNãoModelado; não processa hoje (§15)
Use quandoVocê já tem todos os dadosFalta buscar dado antes de decidir

A Platform não substitui o Engine — ela o consome. Toda execução de esteira termina numa tabela DMN que vive no Engine, e é o Engine que responde qual versão da política rodou. Se você usa só o Engine, você tem regra sem esteira; se tentasse usar só a Platform, não teria o que executar.

Building blockComo se relacionaObrigatório
Decision EngineFornece a política. A esteira lê a última versão publicada da decisão apontadaSim
IAMEmite o token; todas as rotas exigem organizationId e permissãoSim
File StorageCompartilha a infraestrutura S3 que a fonte do tipo S3Não
Webhooks EngineFornece o secureFetch anti-SSRF usado no callback; pode entregar os eventos decision-platform.*Não
Pricing EngineNão é chamado pela esteira. Entre como fonte HTTP, ou depois da decisãoNão
Calculations EngineNão é chamado pela esteira. Amortização, IOF e CET depois que a decisão aprovouNão
Audit TrailRegistra quem publicou qual esteira; a execução é registrada aqui mesmoNão
  ┌───────────────┐
  │ Seu aplicativo│  POST /decision-platform/api/v1/execute
  └───────┬───────┘
          ▼
  ┌─────────────────────────── Decision Platform ─────────────────────────────┐
  │                                                                            │
  │   fontes declaradas          junta            aplica a política            │
  │   ┌────────────┐                                                           │
  │   │ HTTP bureau│──┐                                                        │
  │   ├────────────┤  │      mergedInput      ┌─────────────────────────┐      │
  │   │ S3 arquivo │──┼────────────────────▶  │    Decision Engine      │      │
  │   ├────────────┤  │                       │  última versão PUBLISHED│      │
  │   │ Redis fila │──┘                       │  da decisão apontada    │      │
  │   └────────────┘                          └───────────┬─────────────┘      │
  │        ▲                                              │ HTTP               │
  │        │ o Pricing Engine e o Calculations            ▼                    │
  │        │ Engine entram AQUI, como fonte HTTP    ┌──────────────┐           │
  │        │ — não há integração dedicada           │  runner DMN  │           │
  │        │                                        └──────────────┘           │
  │   ┌────┴──────────┐  ┌──────────────────┐                                  │
  │   │ Pricing Engine│  │Calculations Engine│                                 │
  │   └───────────────┘  └──────────────────┘                                  │
  └────────────────────────────────┬───────────────────────────────────────────┘
                                   ▼
                       ┌────────────────────────┐
                       │ decisão + timing +     │  ─────▶ Webhooks Engine
                       │ registro auditável     │         (eventos da esteira)
                       └───────────┬────────────┘
                                   ▼
                       ┌────────────────────────┐
                       │ APROVADO → precificar  │  Pricing Engine
                       │            e calcular  │  Calculations Engine
                       └────────────────────────┘

  O argumento comercial em uma imagem: uma proposta entra, uma decisão sai,
  e cada peça do caminho é um building block que o cliente já tem contratado.
  A alternativa de mercado é integrar quatro fornecedores para desenhar isto.

Sobre precificação e cálculo, sem rodeio. A esteira não chama o Pricing Engine nem o Calculations Engine — não existe integração dedicada no código. Há dois caminhos honestos: expor o endpoint deles como uma fonte de dados do tipo HTTP, o que os traz para dentro da esteira antes da decisão; ou chamá-los do seu aplicativo depois que a decisão aprovou, que é a ordem natural — primeiro decide se empresta, depois a que taxa e em quantas parcelas.


13Configuração e operação

Variáveis de ambiente

VariávelDescriçãoObrigatóriaPadrão
DATABASE_URLPostgreSQL. Precisa alcançar os schemas platform e decisionsSim
REDIS_URLRedis. Fila assíncrona e fontes do tipo REDIS_STREAMSim
DMN_ENGINE_URLRunner DMN. A Platform o alcança diretamente, não via serviço decision-engineSim (na prática)http://localhost:8080
JWT_SECRETSegredo HS256 do IAM, mínimo 44 caracteresSim
S3_ENDPOINT, S3_BUCKET, S3_REGIONAcesso S3 para fontes do tipo S3Se usar S3
S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEYCredenciais S3. Ambas ou nenhuma — configuração parcial derruba o bootSe usar S3
S3_FORCE_PATH_STYLENecessário com MinIONão
PORTPorta no modo standaloneNão3000 (mapeada para 3011)
DEPLOYMENT_MODEmonolith ou standaloneNãomonolith
MODULE_SELFIdentificação nos health checksNãodecision-platform

MODULE_DECISION_ENGINE_URL aparece no compose para este serviço, mas não é usada pelos caminhos de execução: a Platform fala com o Decision Engine no próprio processo, por repositório Prisma e pelo DmnProxyService.

Dependências de infraestrutura

DependênciaPara quê
PostgreSQLSchema platform, mais leitura do schema decisions do Decision Engine
RedisStream decision-platform:executions e fontes do tipo REDIS_STREAM
S3 (ou MinIO)Fontes do tipo S3
Runner DMNAvaliação da política
IAMEmissão e verificação do token

Limites

LimiteValor
defaultTimeoutMs e maxTimeoutMs da esteira1000 a 300000 ms; padrão 30000
timeoutMs da fonte1000 a 300000 ms; padrão 5000
priority da fonte0 a 100
Nome da fonte100 caracteres, único por esteira
Chave da esteira100 caracteres, ^[a-z0-9-]+$, única por organização
VersãoSemver estrito, única por esteira
traceId255 caracteres
Tentativas de callback3, com esperas de 1 s, 5 s e 15 s
Timeout do callback10000 ms, fixo em código
Página padrão / máxima20 / 100 itens
maxConcurrentExecutions (org)Padrão 100 — não aplicado (§15)
executionRetentionDays (org)Padrão 30 — não aplicado (§15)

Catálogo de erros

StatusCódigoSignificaO que fazer
400VALIDATIONCorpo reprovado no ZodConfira tipos e faixas contra §9
400VALIDATIONInvalid typeConfig for <TIPO>A configuração não bate com o tipo da fonte
400VALIDATIONNo published version found for this configCrie e publique uma versão da esteira
400VALIDATIONRequired data source '<nome>' failedA fonte obrigatória caiu ou estourou o timeout
400VALIDATIONNo published decision version foundA decisão no Decision Engine não tem versão publicada
400VALIDATIONDMN execution failedConfira se as chaves do mergedInput casam com a tabela
400VALIDATIONSSRF protection: ...O destino HTTP ou de callback é interno e foi bloqueado
400VALIDATIONS3 key must be under organization prefixPrefixe a chave com {organizationId}/ ou shared/
400VALIDATIONPARQUET format is not yet supportedUse JSON
403Organization context requiredAutentique informando a organização
403FORBIDDENPermissão faltando, ou recurso de outra organizaçãoConfira a permissão exata em §9
404NOT_FOUNDPlatform config — chave inexistente nesta organizaçãoConfira o configKey
408TIMEOUTFonte HTTP ou S3 estourou o timeoutMsAumente o timeout, ou marque a fonte como opcional
409CONFLICTChave, versão ou nome de fonte em usoEscolha outro
500INTERNALFalha ao buscar fontes, ou falha ao publicar eventoVerifique Redis, S3 e o destino HTTP

Observabilidade.

  • GET /decision-platform/health responde com nome e versão do build. Não verifica banco, Redis, S3 nem o runner DMN.
  • A tabela platform_executions é o painel: status, executionTimeMs, dataFetchTimeMs, dmnExecutionMs, callbackAttempts e callbackLastError. Toda pergunta operacional sobre a esteira se responde por consulta a ela.
  • Eventos publicados: decision-platform.config.{created,updated,deleted}, decision-platform.version.{created,published,archived}, decision-platform.data-source.{created,updated,deleted}, decision-platform.execution.{started,completed,failed,timeout,cancelled}, decision-platform.callback.{delivered,failed} e decision-platform.org-config.updated. Os eventos de timeout e cancelled estão declarados e nenhum caminho de código os emite.
  • Monitore o tamanho do stream decision-platform:executions no Redis. Enquanto §15 valer, ele só cresce.

14Segurança e compliance

Isolamento entre tenants. Toda rota aplica o requireOrganization local, e o organizationId vem do claim assinado — nunca do corpo. DecisionPlatformConfig e PlatformExecution carregam a coluna organization_id. PlatformConfigVersion, PlatformDataSource e as execuções acessadas por ID são verificadas por junção com a config dona, comparando config.organizationId com o do token e respondendo 403 ou 404 quando diverge.

Isolamento nas fontes de dados — a parte que importa. As fontes leem de infraestrutura compartilhada, então o isolamento é validado na configuração, não presumido:

  • S3: a key precisa começar com {organizationId}/ ou shared/. Qualquer outro prefixo é recusado com 400. Uma organização não consegue configurar leitura do prefixo de outra.
  • Redis Stream: o streamName precisa começar com {organizationId}: ou shared:, com a mesma recusa.
  • HTTP: passa pelo secureFetch do Webhooks Engine, que bloqueia destinos internos. Sem isso, uma fonte apontando para http://169.254.169.254/ seria SSRF com credencial de nuvem no fim.

O prefixo shared/ e shared: é um caminho deliberado de compartilhamento entre organizações. Ele é seguro na medida em que você controla o que coloca lá — trate esse prefixo como público dentro da instalação.

Callback. A callbackUrl é do cliente e aponta para fora, o que faz dela superfície de SSRF por definição. Ela sai pelo mesmo secureFetch, com timeout de 10 s e no máximo 3 tentativas. O corpo do callback contém a saída da decisão — se a política devolve limite ou motivo de recusa, esse dado trafega para o destino que o cliente configurou. Use HTTPS e um cabeçalho de autenticação em callbackHeaders.

Segredos nas fontes de dados. O typeConfig de uma fonte HTTP normalmente carrega um cabeçalho Authorization com a credencial do bureau. Esse JSONB é gravado sem criptografia e é devolvido pelas rotas de leitura da fonte a quem tiver DECISION_VERSIONS_READ. Mesma coisa com o callbackHeaders da config, que o código grava como JSON simples — o comentário no schema Prisma diz "Encrypted JSON", mas a criptografia não está implementada. Consequências práticas: use credencial de curta duração ou rotacionável quando possível, restrinja DECISION_VERSIONS_READ a quem precisa, e trate o schema platform como base que contém segredo de terceiros.

Dados pessoais nas execuções. platform_executions guarda input, mergedInput e output em JSONB. Numa esteira de crédito, o mergedInput é o registro mais sensível da plataforma: ele reúne, numa linha, o CPF que o cliente mandou e tudo o que o bureau respondeu. Não há criptografia em coluna, não há mascaramento e não há expurgo automático — o executionRetentionDays é armazenado e não aplicado (§15). Defina um processo de expurgo antes de entrar em produção com volume.

Explicabilidade. O art. 20 da LGPD assegura ao titular o direito de solicitar revisão de decisões automatizadas que afetem seus interesses, incluindo as que definem perfil de crédito. O par mergedInput mais output, cruzado com o ExecutionLog do Decision Engine, é a base factual para atender a esse pedido: o que entrou, o que a regra recebeu, qual tabela avaliou e o que saiu. O processo de revisão humana continua sendo seu.

Autenticação e permissões. Bearer JWT do IAM. As permissões usadas são DECISION_VERSIONS_{CREATE,READ,UPDATE,DELETE,PUBLISH}, DECISION_PROJECTS_DELETE, DECISIONS_EXECUTE, DECISIONS_READ e DECISIONS_MANAGE. Trate DECISIONS_EXECUTE como sensível: como o input do chamador sobrescreve os dados das fontes, quem executa pode fixar um score em vez de deixar o bureau respondê-lo.


15Limitações conhecidas

LimitaçãoImpactoSituação
O modo ASYNC não é processadoO AsyncExecutionConsumer existe, está registrado no container e nunca é iniciado — não há chamada a start() em nenhum ponto do código. A execução é criada, enfileirada no stream decision-platform:executions e permanece PENDING indefinidamente. O callback nunca é entregue. O stream cresce sem consumidor.Bloqueante — use SYNC. É a razão de o status ser beta
Os limites da organização não são aplicadosmaxConcurrentExecutions, maxRetries, retryBackoffMs, retryBackoffMultiplier e executionRetentionDays são gravados, devolvidos pela API e nunca lidos pelo motor: getEffectiveConfig não é chamado em lugar nenhum. Não há limite de concorrência nem expurgo automático.Lacuna conhecida — controle isso do seu lado
Segredos de fonte e de callback não são criptografadostypeConfig e callbackHeaders são gravados como JSON simples e devolvidos pela API de leitura. O schema Prisma anota "Encrypted JSON" para callbackHeaders, mas a criptografia não existe.Lacuna conhecida — restrinja a permissão de leitura
A versão da esteira não congela a versão da políticaO configSnapshot guarda o decisionId, não o versionId do DMN. Publicar no Decision Engine muda esteiras já publicadas na hora.Por design; deve ser sabido
Fontes de dados não são versionadasElas pertencem à config. Adicionar, alterar ou remover afeta todas as versões publicadas imediatamente.Por design
Falha de fonte opcional não fica registradaA execução não guarda quais fontes falharam nem por quê. Só o dataFetchTimeMs agregado sobrevive.Lacuna conhecida
Uma decisão por esteiraCada config aponta para exatamente um decisionId. Não há encadeamento com o resultado de uma decisão alimentando outra.Por design hoje; roadmap
TIMEOUT e CANCELLED nunca são atribuídosEstão no enum e nos eventos declarados; nenhum caminho de código os produz. Estouro de timeout vira FAILED.Modelado, não implementado
PARQUET não é suportado em fonte S3Está no schema de configuração e a leitura retorna erro de validação.Modelado, não implementado
retryPendingCallbacks não tem quem chameO método existe no CallbackService e nenhum agendador o invoca. Callback que esgotou as 3 tentativas não é retentado.Lacuna conhecida
consumers/index.ts é um esqueleto vazioO arquivo contém apenas export {} e um comentário de tarefa pendente.Não implementado
A execução não referencia a versão do DMNPlatformExecution.versionId é a versão da esteira. Ligar a execução à versão da política exige cruzar por horário com o ExecutionLog do Decision Engine.Lacuna conhecida — use traceId
/health não verifica dependência nenhumaResponde 200 com banco, Redis, S3 e runner DMN fora.Lacuna conhecida
Sem interface visual da esteiraToda a configuração é por API. Não há tela para o analista montar ou testar a esteira.Roadmap
Sem simulação de esteiraNão há como rodar uma esteira contra um lote de casos e comparar com a anterior.Roadmap
Sem cache de fonte de dadosToda execução consulta todas as fontes. Duas análises do mesmo CPF em sequência são duas consultas ao bureau — e duas cobranças.Roadmap
Acoplamento de banco com o Decision EngineA Platform lê o schema decisions diretamente. Os dois módulos não são independentes no banco, mesmo em standalone.Por design
Permissões não seguem o recursoPublicar esteira exige DECISION_VERSIONS_CREATE; excluir exige DECISION_PROJECTS_DELETE.Conceda papéis pela tabela de §9

16Perguntas frequentes

Qual a diferença entre o Decision Platform e o Decision Engine?

O Engine é a regra; a Platform é a esteira que leva os dados até a regra. Se você já tem todos os dados de entrada em mãos, chame o Engine direto e pronto. Se alguém precisa consultar um bureau, ler um arquivo ou puxar de uma fila antes de decidir, é a Platform — e ela vai terminar chamando uma tabela DMN que vive no Engine de qualquer forma. A Platform não tem regras próprias e nunca terá: essa é a divisão.

Posso usar a Platform sem o Decision Engine?

Não. Toda esteira aponta para uma decisão do Engine, e a execução falha com No decision linked to this platform config se esse elo não existir. A Platform sem o Engine não teria o que executar.

Posso usar o Decision Engine sem a Platform?

Pode, e muita gente deve. Se a sua aplicação já tem os dados e só quer o veredito, o Engine sozinho resolve com menos peças móveis. A Platform só se paga quando existe busca de dado no meio do caminho.

Se eu publicar uma política nova no Engine, a esteira muda sozinha?

Muda, na hora. A esteira sempre usa a última versão publicada da decisão apontada. Publicar uma versão da esteira não protege contra isso, porque o snapshot congela qual decisão, e não qual versão dela. Para ter políticas diferentes em esteiras diferentes, crie decisões separadas no Engine.

Posso usar o modo assíncrono em produção?

Não hoje. A execução é enfileirada e nada a processa: ela fica PENDING para sempre e o callback não sai. Está registrado em §15 e é a razão de o building block estar em beta. Use SYNC a partir da sua própria fila.

O que acontece se o bureau cair no meio de uma análise?

Depende do isRequired daquela fonte. Se for true, a execução falha com Required data source '<nome>' failed e a linha fica FAILED. Se for false, a esteira segue sem os dados dela e a política decide com o que chegou — que é por isso que a tabela DMN deve ter uma linha tratando a ausência do campo.

Como sei se a lentidão é do bureau ou da regra?

Pelo timing que vem em toda resposta e fica gravado: dataFetchMs é a busca, dmnExecutionMs é a avaliação. Lembre que dataFetchMs é o tempo da fonte mais lenta, não a soma, porque a busca é paralela.

Posso colocar o Pricing Engine dentro da esteira?

Pode, como uma fonte de dados do tipo HTTP apontando para o endpoint dele. Não há integração dedicada. Na maioria dos desenhos, porém, a ordem natural é outra: primeiro a esteira decide se empresta, depois o seu aplicativo chama o Pricing Engine para saber a que taxa e o Calculations Engine para montar as parcelas.

Onde ficam guardadas as credenciais do meu bureau?

No typeConfig da fonte, como JSONB no schema platform, sem criptografia, e legíveis por quem tiver DECISION_VERSIONS_READ. Está em §14 e §15. Restrinja essa permissão e prefira credencial rotacionável.

Quanto tempo o histórico de execução é guardado?

Indefinidamente. O executionRetentionDays existe na configuração da organização, tem padrão de 30 dias, e não é aplicado por nenhum processo. Se você precisa de expurgo, hoje ele é seu.

A esteira funciona sem nenhuma fonte de dados?

Funciona. Sem fontes, o mergedInput é igual ao input e a esteira vira um invólucro em volta do Engine — que é o formato do início rápido, e é um jeito razoável de começar antes de plugar o primeiro bureau.


Padrão: PADRAO-DOCUMENTACAO.md · Índice: INDEX.md

Building blocks relacionados