Catalisa.Building Blocks
Catálogo/Decisão/Decision Platform

Decision Platform

Beta

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

26
Endpoints
5
Entidades
3
Provedores
Tenant
Escopo
3011
Porta
2026-04
Desde

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
O que dá para fazer

28 endpoints em 7 recursos.

Explorar a API →
01

Resumo 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.

flowchart LR
  P["Proposta<br/>CPF + valor pedido"] --> E["POST /execute<br/>uma chamada"]
  E --> F["Busca as fontes<br/>que você declarou"]
  F --> M["Junta com o que<br/>você mandou"]
  M --> D["Aplica a política<br/>vigente"]
  D --> V["Veredito<br/>+ tempo de cada etapa<br/>+ registro auditável"]

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

02

O problema

negó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 desenho de hoje, e onde ele quebra

flowchart TB
  A["Proposta chega<br/>CPF + valor"] --> B["Serviço próprio de cola<br/>~2 mil linhas que ninguém quer manter"]
  B --> C1["Chama o bureau"]
  B --> C2["Lê o arquivo interno"]
  B --> C3["Consulta a lista de restrições"]
  C1 --> D["Junta o JSON à mão"]
  C2 --> D
  C3 --> D
  D --> E["Chama a regra"]
  E --> F["Decisão"]

  C1 -. "bureau lento ou fora do ar" .-> X["A análise inteira falha<br/>recusa por erro técnico"]
  B -. "fonte nova = código novo + deploy" .-> Y["Mais uma forma de a esteira quebrar"]
  F -. "só o tempo total é medido" .-> Z["'A análise está lenta'<br/>sem endereço"]

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.


03

Proposta de valor

negó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.

flowchart LR
  subgraph Declaradas["Fontes declaradas na esteira"]
    direction TB
    B["Bureau principal<br/>isRequired: true"]
    S["Arquivo interno<br/>isRequired: false"]
    R["Fila de eventos<br/>isRequired: false"]
  end

  B -->|"falhou"| Aborta["Execução aborta<br/>400 Required data source failed"]
  S -->|"falhou"| Segue["Não contribui — a esteira segue"]
  R -->|"falhou"| Segue
  Segue --> Politica["A política decide com o que chegou<br/>e cai numa faixa mais conservadora"]

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.


04

Casos de uso reais

negó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}}.

flowchart LR
  App["Aplicativo de originação"] -->|"configKey: analise-credito-pessoal<br/>input: cpf, valor"| Esteira

  subgraph Esteira["Esteira analise-credito-pessoal"]
    direction TB
    F1["HTTP · bureau<br/>isRequired: true · 3000 ms · priority 0"]
    F2["S3 · histórico interno<br/>isRequired: false · priority 10"]
    F1 -->|"$.score → score"| Merge["mergedInput"]
    F2 -->|"$.data.comprometimento → comprometimento"| Merge
  end

  Esteira --> DE["Decision Engine<br/>politica-credito-pessoal"]
  DE --> Out["Decisão + timing<br/>dataFetchMs vs dmnExecutionMs"]
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.

flowchart LR
  Start["Execução da esteira"] --> Par["Busca as três fontes EM PARALELO"]
  Par --> P["Bureau principal<br/>isRequired: true"]
  Par --> S["Bureau secundário<br/>isRequired: false"]
  Par --> A["Arquivo interno<br/>isRequired: false"]

  P -->|"respondeu"| Merge["mergedInput"]
  S -->|"fora do ar — resultado ignorado"| Merge
  A -->|"respondeu"| Merge

  Merge --> DMN["Tabela DMN escrita contando com a ausência do campo"]
  DMN --> Dec["Faixa mais conservadora<br/>decisão de negócio, não erro técnico"]

  P -. "se ESTA falhasse" .-> Fail["400 — a execução aborta"]
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.

flowchart LR
  Q["Pergunta do regulador:<br/>como esta proposta foi analisada?"] --> L["GET /executions<br/>filtrado por config e por data"]
  L --> E["GET /executions/:executionId"]

  E --> I["input<br/>o que o chamador mandou"]
  E --> MI["mergedInput<br/>o que a REGRA recebeu"]
  E --> O["output<br/>o veredito"]
  E --> T["timing + traceId"]

  MI --> DE["versionId no Decision Engine"]
  DE --> XML["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.

flowchart LR
  subgraph Prop["Plataforma de orquestração de formato proprietário"]
    direction TB
    P1["A política é escrita no formato do fornecedor"] --> P2["Sair significa reescrever e revalidar a política inteira"]
    P2 --> P3["Projeto de meses num ativo que ninguém quer tocar"]
  end

  subgraph Cat["Catalisa Decision Platform"]
    direction TB
    C1["A política é XML DMN da OMG, no Decision Engine"] --> C2["Sair significa baixar os XMLs por API"]
    C2 --> C3["Custo de saída conhecido no dia da assinatura"]
  end

05

Mercado e diferenciais

negócio

Panorama

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.

flowchart TB
  subgraph G1["Plataformas globais de decisão"]
    direction LR
    A1["Provenir"]
    A2["Taktile"]
    A3["Alloy"]
    A4["Esteira completa + marketplace de dados<br/>+ autoria visual, por cotação corporativa"]
  end

  subgraph G2["Dados e decisão, mercado brasileiro"]
    direction LR
    B1["Trillia — B3"]
    B2["Serasa Experian"]
    B3["Equifax"]
    B4["A decisão vem acoplada aos dados do próprio grupo"]
  end

  subgraph G3["Modelagem estatística"]
    direction LR
    C1["Zest AI"]
    C2["Resolve o modelo, não resolve a orquestração"]
  end

  subgraph G4["Catalisa Decision Platform"]
    direction LR
    D1["Zero integração pronta — você traz o endpoint"]
    D2["Política em formato padrão, custo de saída conhecido"]
    D3["Building block dentro de uma plataforma já contratada"]
  end

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.

Tabela comparativa

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 — então Pricing Engine e Calculations Engine são chamados pelo seu aplicativo com a mesma credencial, sem projeto de integração de identidade. A esteira em si não chama nenhum dos dois; a fronteira está desenhada em §12.

Quando escolher o concorrente

Se o que você precisa é…Escolha
Não integrar bureau nenhum — chegar com integrações prontasProvenir, Alloy
Desenhar a esteira numa tela, arrastando etapas e testando cenáriosTaktile
Dados brasileiros e decisão vendidos juntos — cadastro positivo, modelos setoriaisTrillia (B3), Serasa Experian
Várias decisões encadeadas dentro da mesma esteiraQualquer um deles — nós não fazemos
Execução assíncrona em produção hojeQualquer um deles — leia §15 antes

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.


06

Modelo de cobrança e ROI

negó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
flowchart LR
  Exec["1 execução de esteira"] --> D1["N chamadas de rede<br/>uma por fonte declarada"]
  Exec --> D2["1 avaliação da política<br/>no runner DMN"]
  Exec --> D3["1 linha em platform_executions<br/>input + mergedInput + output em JSONB"]

  D1 --> Custo["Custo da Catalisa"]
  D2 --> Custo
  D3 --> Reten["Custo de retenção<br/>cresce com a janela de auditoria"]

  D1 -. "a consulta em si" .-> Bureau["Contrato SEU com o bureau<br/>não passa pela Catalisa"]

Atenção. 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.


07

Arquitetura

As camadas e o caminho da requisição

flowchart TB
  App["Seu aplicativo"] -->|"HTTP · Bearer JWT do IAM"| Rotas

  subgraph Rotas["Hono app · basePath '/decision-platform'"]
    direction TB
    R1["configsRouter · 7 rotas · esteiras<br/>/api/v1/configs"]
    R2["versionsRouter · 6 rotas · versões da esteira<br/>/api/v1"]
    R3["dataSourcesRouter · 5 rotas · fontes de dados<br/>/api/v1"]
    R4["executionsRouter · 4 rotas · execute + histórico<br/>/api/v1"]
    R5["orgConfigRouter · 4 rotas · limites da organização<br/>/api/v1"]
  end

  Rotas -->|"toda rota: authMiddleware → requirePermission(P) → requireOrganization"| Exec

  subgraph Exec["ExecutionService"]
    direction TB
    E1["executeSync — responde na hora"]
    E2["executeAsync — enfileira no Redis Stream e responde 202"]
  end

  E1 --> DS
  E2 --> Consumer

  subgraph DS["DataSourceService — busca em PARALELO, ordena por priority"]
    direction LR
    H1["S3Handler"]
    H2["RedisHandler"]
    H3["HttpHandler"]
  end

  subgraph Consumer["AsyncExecutionConsumer"]
    direction TB
    K1["grupo 'execution-processors' no stream<br/>'decision-platform:executions'"]
    K2["NÃO É INICIADO HOJE — ver §15"]
  end

  Consumer --> CB["CallbackService<br/>secureFetch, 3 tentativas"]

  DS -->|"mergedInput = dados das fontes + input do chamador"| DE

  subgraph DE["Decision Engine — no próprio processo"]
    direction TB
    D1["VersionRepository.findLatestPublished(decisionId) → XML DMN"]
    D2["DmnProxyService.execute(dmn, mergedInput)"]
  end

  D2 -->|"HTTP"| Runner["runner DMN"]

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

Uma proposta entra por POST /decision-platform/api/v1/execute, com { "configKey": "analise-credito", "input": {...} }, e sai um veredito. As cinco etapas abaixo são o produto inteiro.

flowchart TB
  Prop["Proposta<br/>CPF, valor"] -->|"POST /decision-platform/api/v1/execute"| S1

  S1["1 · RESOLVE A ESTEIRA<br/>config pela chave → versão publicada mais recente"] --> S2

  subgraph S2["2 · BUSCA OS DADOS — todas as fontes EM PARALELO, ordenadas por priority"]
    direction LR
    F1["HTTP · bureau<br/>required · 3000 ms"]
    F2["S3 · histórico<br/>opcional · 5000 ms"]
    F3["REDIS_STREAM · eventos<br/>opcional · 2000 ms"]
  end

  S2 -->|"inputMapping por fonte: $.score → score"| S3
  S2 -. "required que falha" .-> Erro400["400 — a execução aborta"]
  S2 -. "opcional que falha" .-> S3

  S3["3 · JUNTA<br/>mergedInput = dados das fontes + input do chamador<br/>o input do chamador SOBRESCREVE o das fontes em colisão"] --> S4

  S4["4 · APLICA A POLÍTICA<br/>Decision Engine → última versão PUBLISHED da decisão apontada<br/>→ runner DMN avalia a tabela"] --> S5

  S5["5 · REGISTRA<br/>PlatformExecution: input, mergedInput, output, traceId,<br/>timing com totalMs, dataFetchMs e dmnExecutionMs"] --> Sync
  S5 --> Async

  Sync["SYNC: 200 com a decisão"]
  Async["ASYNC: 202 na hora, callback depois<br/>o processador não roda hoje — §15"]

Atenção. 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.

A mesma execução, vista como conversa

sequenceDiagram
  autonumber
  participant App as Seu aplicativo
  participant DP as Decision Platform
  participant PG as PostgreSQL schema platform
  participant Bur as Bureau via HTTP
  participant S3 as S3 com o histórico
  participant DE as Decision Engine no próprio processo
  participant Run as runner DMN

  App->>DP: POST /execute com configKey, input e traceId
  DP->>PG: acha a config pela chave e a última versão PUBLISHED
  DP->>PG: cria PlatformExecution e marca RUNNING
  Note over DP: publica decision-platform.execution.started

  par Busca em paralelo
    DP->>Bur: consulta com o timeoutMs da fonte
    Bur-->>DP: score e restrições
  and
    DP->>S3: lê o objeto sob o prefixo da organização
    S3-->>DP: comprometimento
  end

  Note over DP: mergedInput = dados das fontes + input do chamador
  DP->>PG: grava mergedInput
  DP->>DE: findLatestPublished do decisionId do snapshot
  DE-->>DP: XML DMN da política vigente
  DP->>Run: execute com o DMN e o mergedInput
  Run-->>DP: output
  DP->>PG: marca COMPLETED com totalMs, dataFetchMs e dmnExecutionMs
  DP-->>App: 200 com output e timing

Decisões não óbvias — a execução

  • 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.

Decisões não óbvias — versionamento e isolamento

flowchart LR
  subgraph Congela["O configSnapshot da versão CONGELA"]
    direction TB
    A1["nome e chave"]
    A2["defaultTimeoutMs e maxTimeoutMs"]
    A3["callbackUrl"]
    A4["decisionId — QUAL decisão"]
    A5["decisionKey — QUAL nó do grafo"]
  end

  subgraph NaoCongela["O configSnapshot NÃO congela"]
    direction TB
    B1["a versão do DMN — sempre a última PUBLISHED"]
    B2["a lista de fontes de dados — pertence à config"]
  end

  B1 -.->|"publicar no Decision Engine"| Efeito["muda esteiras já publicadas, na hora"]
  B2 -.->|"criar ou remover fonte"| Efeito
  • O registro da execução guarda qual versão decidiu. PlatformExecution.decisionVersionId e decisionVersionNumber são gravados no momento em que a decisão sai, junto com o trace de regras avaliadas. É isso que responde "por que este caso foi recusado em março" depois que a política mudou — o prazo de dez dias do art. 5º, VI da Lei 12.414 e o direito à explicação do art. 20 da LGPD dependem disso. Repare na distinção: a esteira não fixa uma versão, mas o registro sabe qual valia.
  • A versão da esteira congela a configuração, não a regra. O configSnapshot da PlatformConfigVersion guarda nome, chave, timeouts, callbackUrl, decisionId e decisionKey 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.
  • decisionKey escolhe qual nó do grafo responde. Uma política DMN pode ter várias decisões encadeadas — capacidade, risco, elegibilidade, limite. Sem decisionKey, o motor avalia o primeiro nó do arquivo e devolve 200 com uma resposta perfeitamente plausível e errada: um limite de crédito voltando "ALTA". A esteira passa o nó ao motor a partir do snapshot.

    Ao atualizar um ambiente que já rodava sem decisionKey: subir o código e aplicar as migrações não conserta esteiras existentes, porque o snapshot foi congelado quando a versão foi criada. Para cada esteira: PATCH /configs/:id com o decisionKey, depois criar e publicar uma versão nova da esteira. GET /configs?pageSize=100 lista o que precisa de ajuste.

  • 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).


08

Conceitos 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, decisionKey, 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, decisionVersionId, decisionVersionNumber, trace, 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 · BUREAU
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

A esteira e a sua versão compartilham o mesmo ciclo de três estados. São dois objetos e dois publishes distintos.

stateDiagram-v2
  direction LR
  [*] --> DRAFT: criação
  DRAFT --> PUBLISHED: POST /configs/:id/publish<br/>ou POST /versions/:id/publish
  PUBLISHED --> ARCHIVED: POST /versions/:id/archive
  ARCHIVED --> [*]

  note right of PUBLISHED
    Só versão PUBLISHED executa.
    Arquivar uma DRAFT retorna 400.
  end note

Atenção. 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.

A execução tem a sua própria máquina, e ela se comporta de forma diferente em SYNC e em ASYNC.

stateDiagram-v2
  direction LR
  [*] --> PENDING: criada — só em ASYNC
  PENDING --> RUNNING: o processador pega
  [*] --> RUNNING: em SYNC, já nasce e roda
  RUNNING --> COMPLETED
  RUNNING --> FAILED
  RUNNING --> TIMEOUT
  RUNNING --> CANCELLED
  COMPLETED --> [*]
  FAILED --> [*]
  TIMEOUT --> [*]
  CANCELLED --> [*]

Atenção. 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. Estouro de timeout vira FAILED — ver §15.

Como as entidades se ligam

erDiagram
  DecisionPlatformConfig ||--o{ PlatformConfigVersion : "versiona"
  DecisionPlatformConfig ||--o{ PlatformDataSource : "declara fontes"
  DecisionPlatformConfig ||--o{ PlatformExecution : "executa"
  PlatformConfigVersion ||--o{ PlatformExecution : "qual versão da esteira rodou"
  DecisionPlatformConfig }o--|| Decision : "aponta para uma decisão do Decision Engine"
  OrganizationPlatformConfig ||--|| Organizacao : "limites por organização"

  DecisionPlatformConfig {
    string key "único por organização"
    string decisionId
    enum status "DRAFT PUBLISHED ARCHIVED"
    int defaultTimeoutMs
    int maxTimeoutMs
    string callbackUrl
    json callbackHeaders "sem criptografia — ver 14"
  }
  PlatformConfigVersion {
    string version "semver, único por config"
    enum status
    json configSnapshot "congela decisionId, NÃO a versão do DMN"
    datetime publishedAt
  }
  PlatformDataSource {
    string name "único por config"
    enum type "S3 REDIS_STREAM HTTP"
    json typeConfig "sem criptografia — ver 14"
    json inputMapping
    int priority "ordem de mesclagem"
    bool isRequired
    int timeoutMs
  }
  PlatformExecution {
    json input "o que o chamador mandou"
    json mergedInput "o que a REGRA recebeu"
    json output
    string error
    int executionTimeMs
    int dataFetchTimeMs
    int dmnExecutionMs
    int callbackAttempts
    string traceId "sua correlação, indexada"
  }
  OrganizationPlatformConfig {
    int maxConcurrentExecutions "armazenado, não aplicado"
    int maxRetries "armazenado, não aplicado"
    int retryBackoffMs "armazenado, não aplicado"
    int retryBackoffMultiplier "armazenado, não aplicado"
    int executionRetentionDays "armazenado, não aplicado"
  }

09

Referê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.

Os 26 endpoints se organizam em cinco recursos, e a hierarquia entre eles explica a ordem em que você os chama:

flowchart TB
  Cfg["configs · 7 rotas<br/>a esteira"] --> Ver["versions · 6 rotas<br/>versão da esteira"]
  Cfg --> DSrc["data-sources · 5 rotas<br/>fontes de dados"]
  Cfg --> Exe["execute + executions · 4 rotas<br/>execução e histórico"]
  Ver --> Exe
  DSrc --> Exe
  Org["org-config · 4 rotas<br/>limites da organização"] -.->|"armazenado, não aplicado — §15"| Exe

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, decisionKey, 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.

Repetir uma chamada com segurança

bash
curl -s -X POST "$API/decision-platform/api/v1/execute" \
  -H "X-API-Key: $CHAVE" -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: proposta-4821' \
  -d '{"configKey":"credito","input":{"cpf":"...","renda":9000}}'
curl -s -X POST "$API/decision-platform/api/v1/execute" \
  -H "X-API-Key: $CHAVE" -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: proposta-4821' \
  -d '{"configKey":"credito","input":{"cpf":"...","renda":9000}}'

A conexão caiu e você não sabe se a decisão saiu? Repita a chamada com a mesma chave. Se a primeira tiver chegado, você recebe a mesma decisão, com o mesmo executionId; se não, ela é tomada agora. A esteira lê fonte externa a cada execução, então decidir de novo pode dar outra resposta — é isso que a chave evita, antes mesmo de evitar a segunda cobrança.

Uso — /decision-platform/api/v1

MétodoRotaDescriçãoPermissão
GET/decision-platform/api/v1/usageConsumo do período, para cobrar e para a tela do clienteDECISIONS_READ

Parâmetros: from e to (ISO; padrão é o mês civil de São Paulo corrente), groupBy (day ou month), environment (live, test ou all — honrado só para quem não está preso a um ambiente).

Resposta: totals com billable (o que se cobra), failed (morreu sem decidir) e test (sandbox), mais a series por período. A soma vem dos dois caminhos de execução.

bash
curl -s "$API/decision-platform/api/v1/usage?groupBy=day" \
  -H "X-API-Key: $CHAVE" | jq '.data.attributes.totals'
# { "billable": 8412, "failed": 37, "test": 5100 }
curl -s "$API/decision-platform/api/v1/usage?groupBy=day" \
  -H "X-API-Key: $CHAVE" | jq '.data.attributes.totals'
# { "billable": 8412, "failed": 37, "test": 5100 }

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

json
{
  "configKey": "analise-credito-pessoal",
  "mode": "SYNC",
  "input": { "cpf": "12345678901", "valorSolicitado": 10000 },
  "traceId": "proposta-2026-08-16-00042",
  "timeoutMs": 15000
}
{
  "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

json
{
  "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..." }
}
{
  "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

json
{
  "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..." }
}
{
  "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

json
{
  "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
}
{
  "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.

json
{
  "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": "..."
    }
  }
}
{
  "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.


10

Iní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á.

São oito passos, e os dois publishes do meio são o que mais confunde na primeira vez:

flowchart LR
  P1["1 · Autenticar<br/>no IAM"] --> P2["2 · Descobrir a decisão<br/>no Decision Engine"]
  P2 --> P3["3 · Criar a esteira<br/>POST /configs"]
  P3 --> P4["4 · Declarar uma fonte<br/>opcional"]
  P4 --> P5["5 · Criar e publicar<br/>a VERSÃO"]
  P5 --> P6["6 · Publicar<br/>a CONFIG"]
  P6 --> P7["7 · Executar<br/>POST /execute"]
  P7 --> P8["8 · Ver o que a regra<br/>realmente recebeu"]

1. Autenticar

bash
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
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

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

bash
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')

echo $CONFIG_ID
# 4f7a1c62-... — a esteira nasce em DRAFT
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')

echo $CONFIG_ID
# 4f7a1c62-... — a esteira nasce em DRAFT

A key precisa casar com ^[a-z0-9-]+$ e é única na sua organização.

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

bash
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'
# "idade-externa"
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'
# "idade-externa"

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

bash
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"
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

bash
curl -s -X POST "$BASE/configs/$CONFIG_ID/publish" \
  -H "Authorization: Bearer $TOKEN" | jq '.data.attributes.status'
# "PUBLISHED"
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

bash
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
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
json
{
  "data": {
    "type": "platform-execution",
    "id": "9c2f...",
    "attributes": {
      "status": "COMPLETED",
      "mode": "SYNC",
      "output": { "result": "adult" },
      "timing": { "totalMs": 68, "dataFetchMs": 21, "dmnExecutionMs": 39 }
    }
  }
}
{
  "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

bash
curl -s "$BASE/executions" -H "Authorization: Bearer $TOKEN" \
  | jq '.data[0].attributes | {input, mergedInput, timing}'
curl -s "$BASE/executions" -H "Authorization: Bearer $TOKEN" \
  | jq '.data[0].attributes | {input, mergedInput, timing}'
json
{
  "input":       { "age": 25 },
  "mergedInput": { "age": 25 },
  "timing":      { "totalMs": 68, "dataFetchMs": 21, "dmnExecutionMs": 39 }
}
{
  "input":       { "age": 25 },
  "mergedInput": { "age": 25 },
  "timing":      { "totalMs": 68, "dataFetchMs": 21, "dmnExecutionMs": 39 }
}

Nesta esteira de exemplo o input e o mergedInput são iguais, porque a fonte é opcional e o targetField dela colide com o campo que você mandou — e o input do chamador vence. Assim que uma fonte trouxer um campo que você não enviou, a diferença entre os dois aparece.

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


11

Receitas

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.

flowchart LR
  B["bureau-principal · HTTP<br/>priority 0 · isRequired true · 3000 ms"] --> M["mergedInput"]
  H["historico-interno · S3<br/>priority 10 · isRequired false · 5000 ms"] --> M
  M --> DMN["a política decide"]
  B -. "falhou" .-> Abort["400 — sem decisão"]
  H -. "falhou" .-> M

1. Declarar o bureau principal — sem ele não há decisão

bash
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
  }'
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
  }'
json
{ "data": { "type": "platform-data-source", "id": "…",
            "attributes": { "name": "bureau-principal", "isRequired": true } } }
{ "data": { "type": "platform-data-source", "id": "…",
            "attributes": { "name": "bureau-principal", "isRequired": true } } }

2. Declarar o histórico interno em S3 — se faltar, a política decide mais conservadora

bash
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
  }'
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
  }'
json
{ "data": { "type": "platform-data-source", "id": "…",
            "attributes": { "name": "historico-interno", "isRequired": false } } }
{ "data": { "type": "platform-data-source", "id": "…",
            "attributes": { "name": "historico-interno", "isRequired": false } } }

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.

sequenceDiagram
  autonumber
  participant Voce as Você
  participant DE as Decision Engine
  participant DP as Decision Platform

  Voce->>DE: POST /decisions/:id/versions com o DMN revisado
  DE-->>Voce: versão 1.1.0 em DRAFT
  Voce->>DE: POST /versions/:id/publish
  DE-->>Voce: 1.1.0 PUBLISHED
  Note over DP: nenhuma chamada ao decision-platform
  DP->>DE: próxima execução pede a última versão publicada
  DE-->>DP: 1.1.0 — a esteira já usa a política nova
bash
# 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.
# 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 a execução pela sua correlação ou pelo intervalo

bash
curl -s "$BASE/executions?filter[configId]=$CONFIG_ID&filter[from]=2026-08-01" \
  -H "Authorization: Bearer $TOKEN" | jq '.data[] | {id, traceId: .attributes.traceId}'
curl -s "$BASE/executions?filter[configId]=$CONFIG_ID&filter[from]=2026-08-01" \
  -H "Authorization: Bearer $TOKEN" | jq '.data[] | {id, traceId: .attributes.traceId}'
json
{ "id": "9c2f…", "traceId": "proposta-2026-08-16-00042" }
{ "id": "9c2f…", "traceId": "proposta-2026-08-16-00042" }

2. Abrir a execução completa, com o que a regra recebeu

bash
curl -s "$BASE/executions/$EXEC_ID" -H "Authorization: Bearer $TOKEN" \
  | jq '.data.attributes | {input, mergedInput, output, timing, versionId}'
curl -s "$BASE/executions/$EXEC_ID" -H "Authorization: Bearer $TOKEN" \
  | jq '.data.attributes | {input, mergedInput, output, timing, versionId}'

O mergedInput é o campo que torna a análise reproduzível: ele guarda o que o bureau respondeu naquele instante.

3. Recuperar a REGRA que rodou, do lado do Decision Engine

bash
curl -s "$ENGINE/decisions/$DECISION_ID/execution-logs?filter[from]=..." \
  -H "Authorization: Bearer $TOKEN" | jq '.data[0].attributes.versionId'
curl -s "$ENGINE/decisions/$DECISION_ID/execution-logs?filter[from]=..." \
  -H "Authorization: Bearer $TOKEN" | jq '.data[0].attributes.versionId'
flowchart LR
  Exec["PlatformExecution<br/>versionId = versão da ESTEIRA"] -->|"cruza por horário e traceId"| Log["ExecutionLog do Decision Engine<br/>versionId = versão do DMN"]
  Log --> XML["GET /versions/:id → o XML que avaliou a proposta"]

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

bash
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)
    }'
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.

bash
# 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}"
# 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.


12

Integraçã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.

flowchart TD
  Q{"Você já tem em mãos<br/>todos os dados que a regra precisa?"}
  Q -->|"Sim"| E["Decision Engine<br/>POST /decisions/:id/execute"]
  Q -->|"Não — falta bureau, arquivo ou fila"| P["Decision Platform<br/>POST /execute com configKey"]
  P -->|"termina sempre em"| E
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 S3 lêNã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
flowchart TB
  App["Seu aplicativo"] -->|"POST /decision-platform/api/v1/execute"| DP

  subgraph DP["Decision Platform"]
    direction TB
    subgraph Fontes["Fontes declaradas"]
      direction TB
      F1["HTTP · bureau"]
      F2["S3 · arquivo"]
      F3["Redis · fila"]
    end
    Merge["junta → mergedInput"]
    F1 --> Merge
    F2 --> Merge
    F3 --> Merge
  end

  Merge -->|"aplica a política"| DE["Decision Engine<br/>última versão PUBLISHED da decisão apontada"]
  DE -->|"HTTP"| Run["runner DMN"]

  DP --> Saida["decisão + timing + registro auditável"]
  Saida --> WH["Webhooks Engine<br/>eventos da esteira"]

  subgraph Depois["Depois da decisão — chamado pelo SEU aplicativo"]
    direction LR
    PE["Pricing Engine<br/>a que taxa"]
    CE["Calculations Engine<br/>em quantas parcelas"]
  end

  Saida -->|"APROVADO"| Depois

  PE -. "ou ANTES, entrando como fonte HTTP" .-> F1
  CE -. "ou ANTES, entrando como fonte HTTP" .-> F1

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.

Atenção. O Pricing Engine e o Calculations Engine aparecem no desenho como caminhos possíveis, não como integração existente. Não há chamada dedicada a nenhum dos dois no código da esteira — as setas pontilhadas são as duas formas de encaixá-los, descritas logo abaixo.

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.


13

Configuraçã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
flowchart LR
  DP["decision-platform<br/>porta 3011"]
  DP --> PG1["PostgreSQL · schema platform"]
  DP --> PG2["PostgreSQL · schema decisions<br/>leitura direta — acoplamento com o Decision Engine"]
  DP --> RD["Redis · stream decision-platform:executions<br/>e fontes REDIS_STREAM"]
  DP --> S3["S3 ou MinIO · fontes S3"]
  DP --> RUN["runner DMN · DMN_ENGINE_URL"]
  DP --> IAM["IAM · emissã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
403—Organization 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.

14

Segurança e compliance

Isolamento entre tenants

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 pela esteira dona. Fora do escopo a resposta é sempre 404, igual a um id inexistente: um 403 confirmaria a quem pergunta que o recurso existe.

Isolamento entre os clientes de um revendedor (subcontas)

Quem revende a esteira como SaaS atende cada cliente como uma subconta dentro da própria organização. Nesse modelo o organizationId sozinho não separa mais nada, porque todos os clientes o compartilham. A fronteira passa a ser a subconta, com o mesmo mecanismo do Biometrics: chave de API presa à subconta, ou a chave da organização com o header X-Subaccount-Id.

O queRegra
Quem vê o quêA subconta vê só o que é dela. A organização (JWT, ou chave sem X-Subaccount-Id) vê tudo. Fora do escopo = 404.
Chaves de nomeA key da esteira é única por dono. Dois clientes podem ter, cada um, uma esteira credito, sem colisão e sem que um descubra o outro por um 409.
POST /executeO configKey é resolvido sob o dono exato de quem chama. O credito do cliente A nunca executa o credito do cliente B. Para a organização executar a esteira de um cliente, ela manda o X-Subaccount-Id desse cliente.
Esteira e políticaUma esteira só aponta para uma política do mesmo dono. A execução resolve a política pelo id, então uma esteira da organização apontada para a política de um cliente rodaria fora do escopo, da cota e da cobrança dele — isso é recusado na criação.
Registro da execuçãoGravado com o dono da esteira, não com o de quem chamou. O consumo pertence a quem é dono.
Eventos e webhooksTodo evento leva metadata.subaccountId do dono do recurso. O Webhooks Engine entrega ao cliente certo, inclusive quando foi a organização que fez a mudança.
Teste e produçãoA chave carrega o ambiente. Execução nasce no ambiente da chave que a criou, e a chave presa a uma subconta só enxerga o próprio mundo. Quem não está preso (console, JWT, chave da organização) filtra com ?environment=live|test|all.
Cota402 QUOTA_EXCEEDED quando o mês civil de São Paulo já gastou a franquia da subconta (Subaccount.monthlyQuota, que o painel escreve a partir do plano). Sem teto = plano pago, e o excedente é cobrado em vez de bloqueado. Subconta suspensa dá 403 SUBACCOUNT_SUSPENDED, inclusive em teste.
Repetir sem decidir de novoO cabeçalho Idempotency-Key no POST /execute. A repetição devolve a decisão já gravada, não avalia a política de novo e não gasta outra decisão. A chave vale 24 h e pertence ao dono: a chave de um cliente nunca devolve a execução de outro. Mesma chave com entrada diferente responde 409 — devolver a primeira decisão seria entregar a resposta errada em silêncio.
O que contaDecisão que chegou a um resultado, pelos dois caminhos — pela esteira e direto na política. Contar só a esteira deixaria o motor como produção ilimitada e de graça. Não conta: execução que morreu sem decidir (FAILED, TIMEOUT, CANCELLED) e nada do ambiente de teste.
Tipos de fonteEsteira de subconta usa fonte HTTP, com o endpoint e a credencial do próprio cliente. S3, REDIS_STREAM e BUREAU compartilham recursos da organização (o prefixo de armazenamento, o contrato de bureau e o cache de consultas), e ficam para quando esses building blocks separarem por subconta. Esteira da organização segue com os quatro tipos.

Linhas criadas antes das subcontas têm subaccount_id nulo, o que quer dizer "da organização" — exatamente quem era dono delas.

A catraca é tests/integration/decision-platform/subaccount-isolation.integration.test.ts: dois clientes montam a mesma esteira, com as mesmas chaves, num Postgres real, e cada um tenta todas as portas do outro — ler, listar, editar, apagar, versionar, publicar, ligar tag, apontar esteira, pendurar fonte e executar. Uma rota nova que esqueça o escopo faz esse arquivo falhar.

Isolamento nas fontes de dados — a parte que importa

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.
flowchart TD
  Cfg["Configuração de uma fonte de dados"] --> T{"Tipo da fonte"}
  T -->|"S3"| S["A key começa com o organizationId ou com shared/ ?"]
  T -->|"REDIS_STREAM"| R["O streamName começa com organizationId: ou shared: ?"]
  T -->|"HTTP"| H["secureFetch — o destino é interno?"]

  S -->|"não"| Rec["400 — a configuração é RECUSADA"]
  R -->|"não"| Rec
  H -->|"sim, é interno"| Rec

  S -->|"sim"| Ok["Fonte aceita"]
  R -->|"sim"| Ok
  H -->|"não, é externo"| Ok

  H -. "sem isso" .-> SSRF["http://169.254.169.254/ seria SSRF<br/>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

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

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

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 e o art. 20 da LGPD

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

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.


15

Limitaçõ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. O registro da execução, porém, guarda qual versão decidiu — publicar uma política nova não reescreve o passado.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 DMNResolvido. A execução grava decisionVersionId e decisionVersionNumber — a versão da política que decidiu aquele caso, no instante em que ele foi decidido. Também grava o trace devolvido pelo motor DMN, que antes era calculado e descartado. Os três voltam em GET /api/v1/executions/:id.Resolvido
/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

16

Perguntas 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