Decision Platform
BetaA esteira que busca os dados, aplica a política e devolve o veredito
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.
- 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
- 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
- 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
28 endpoints em 7 recursos.
/decision-platform/api/v1/configs/decision-platform/api/v1/decision-platform/api/v1/decision-platform/api/v1/decision-platform/api/v1/org-config/decision-platform/api/v1/usage/decision-platform/healthResumo 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).
| Atributo | Valor |
|---|---|
| Identificador | decision-platform |
| Categoria | Decisão |
| Escopo | Tenant (exige organizationId no token) |
| Porta (standalone) | 3011 |
| Path alias | @decision-platform |
| Prefixo HTTP | /decision-platform |
| Schema no banco | platform |
| Status | Beta desde 2026-04 |
| Depende de | PostgreSQL, Redis, S3, runner DMN, Decision Engine, IAM |
O problema
negócioO 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.timeoutque 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.
Proposta de valor
negócio| Antes | Depois |
|---|---|
| Um microserviço próprio só para orquestrar bureau e regra | Uma config com fontes declaradas e uma chamada de API |
| Fonte nova é código novo e deploy | Fonte nova é POST .../data-sources |
| Timeout escondido no cliente HTTP | timeoutMs por fonte, com isRequired explícito |
| "A análise está lenta" sem endereço | timing com dataFetchMs e dmnExecutionMs separados |
| A política vive no formato do fornecedor | A 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.
Casos de uso reais
negócioCaso 1 — Uma esteira de crédito que enriquece a proposta antes de decidir Cenário ilustrativo
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 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.
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 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
A mesma esteira, agora com três fontes: bureau principal, bureau secundário de enriquecimento e arquivo interno.
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.
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"]
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
Operação regulada precisa demonstrar como uma proposta específica foi analisada.
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.
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"]
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
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.
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.)
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"]
endMercado e diferenciais
negócioPanorama
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"]
endO 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ério | Catalisa Decision Platform | Provenir | Taktile | Trillia (B3) | Alloy |
|---|---|---|---|---|---|
| Preço | Precificação em definição | Não público | Não público | Não público | Não público |
| Integrações de dados prontas | Nenhuma — você traz o endpoint | 120+ parceiros | Marketplace próprio | Ecossistema B3 | 270+ soluções |
| Formato da política | DMN (XML da OMG), portável | Proprietário | Proprietário | Proprietário | Proprietário |
| Autoria visual da esteira | Não (ver §15) | Sim | Sim, é a força deles | Sim | Sim |
| Execução assíncrona com callback | Modelada, não processa hoje (§15) | Sim | Sim | Sim | Sim |
| Timing por etapa na resposta | Sim, dataFetch e dmn separados | Não documentado publicamente | Não documentado publicamente | Não documentado publicamente | Não documentado publicamente |
| Fonte opcional que falha | Não derruba a decisão | Sim | Sim | Sim | Sim |
| Foco brasileiro | Plataforma brasileira | Escritório em São Paulo | Global | Brasileiro, é a força deles | Global |
| Decisões encadeadas na esteira | Não — uma decisão por config | Sim | Sim | Sim | Sim |
Nossos diferenciais
- 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.
- O tempo de cada etapa vem na resposta.
timing.dataFetchMsetiming.dmnExecutionMsseparados, em toda execução, gravados no banco. É pouco glamouroso e é o que resolve a discussão sobre latência de esteira. - Essencial e complementar é configuração, não código.
isRequiredetimeoutMspor 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. - A esteira já nasce dentro da plataforma. Ela usa o mesmo token, o mesmo
organizationIde 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 prontas | Provenir, Alloy |
| Desenhar a esteira numa tela, arrastando etapas e testando cenários | Taktile |
| Dados brasileiros e decisão vendidos juntos — cadastro positivo, modelos setoriais | Trillia (B3), Serasa Experian |
| Várias decisões encadeadas dentro da mesma esteira | Qualquer um deles — nós não fazemos |
| Execução assíncrona em produção hoje | Qualquer 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.
Modelo de cobrança e ROI
negócioUnidade 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.
| Driver | Por que ele importa |
|---|---|
| Execuções por mês | É a unidade de valor entregue |
| Fontes de dados por esteira | Cada fonte é uma chamada de rede e um timeout a sustentar por execução |
| Retenção do histórico | Cada 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 Platform | Provenir | Taktile | Alloy | |
|---|---|---|---|---|
| Licença da plataforma | Precificação em definição | Não público — cotação | Não público — cotação | Não público — cotação |
| Custo do dado de bureau | Contratado por você, direto | Pode vir pelo marketplace | Pode vir pelo marketplace | Pode vir pelo marketplace |
| Integração das 3 fontes | Configuração; sem código | Provavelmente pronta | Provavelmente pronta | Provavelmente pronta |
| Migrar a política para fora | Baixo — XML DMN por API | Alto — formato proprietário | Alto — formato proprietário | Alto — 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.
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 timingDecisões não óbvias — a execução
- O input do chamador vence o das fontes. A mesclagem é
{ ...mergedData, ...input.input }. Se o bureau devolveuscore: 700e o chamador mandouscore: 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 — trateDECISIONS_EXECUTEcomo permissão sensível. - As fontes são buscadas em paralelo, e
prioritysó ordena a mesclagem.prioritynã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
DataSourceServicefazunwrapOrno resultado de cada fonte; a falha vira um resultado comsuccess: falsee 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.decisionVersionIdedecisionVersionNumbersão gravados no momento em que a decisão sai, junto com otracede 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
configSnapshotdaPlatformConfigVersionguarda nome, chave, timeouts,callbackUrl,decisionIdedecisionKeyno momento da criação. Ele não guarda a versão do DMN. Na execução, o serviço lê odecisionIddo 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. decisionKeyescolhe qual nó do grafo responde. Uma política DMN pode ter várias decisões encadeadas — capacidade, risco, elegibilidade, limite. SemdecisionKey, o motor avalia o primeiro nó do arquivo e devolve200com 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/:idcom odecisionKey, depois criar e publicar uma versão nova da esteira.GET /configs?pageSize=100lista o que precisa de ajuste.- As fontes de dados pertencem à config, não à versão.
resolveDataSourcesbusca as fontes peloconfigId. 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
VersionRepositorye oDmnProxyServicedo@decision-enginevia container. Isso significa que o serviçodecision-platformem standalone precisa de acesso ao schemadecisionsno banco e deDMN_ENGINE_URLpróprio. Não há chamada HTTP ao serviçodecision-engine: a variávelMODULE_DECISION_ENGINE_URLpresente 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
secureFetchcom proteção contra SSRF. AcallbackUrlé fornecida pelo cliente e aponta para fora. OCallbackServicereusa osecureFetchdo 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
S3DataSourceHandlerexige que a chave comece com{organizationId}/oushared/; oRedisDataSourceHandlerexige que o nome do stream comece com{organizationId}:oushared:. 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).
Conceitos e modelo de dados
Glossário
| Termo | Significa |
|---|---|
| Config (esteira) | A DecisionPlatformConfig. Define qual decisão aplicar, timeouts e callback. Tem chave estável (analise-credito). |
| Versão da esteira | PlatformConfigVersion. Congela a configuração em semver. Não congela a versão da regra. |
configSnapshot | O 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. |
inputMapping | Como extrair da resposta da fonte ($.data.score) e para onde levar (score), com transformação opcional. |
isRequired | Se a falha dessa fonte aborta a execução (true) ou é ignorada (false). |
priority | Ordem de mesclagem, não de execução. Menor é mesclado primeiro, e portanto pode ser sobrescrito. |
mergedInput | O que a regra de fato recebeu: dados das fontes mesclados com o input do chamador. |
| Execution | O registro de uma execução, com entrada, entrada enriquecida, saída, tempos e status. |
traceId | Correlação que você fornece. A plataforma só guarda e indexa. |
| Org config | Limites por organização: concorrência, retentativas e retenção. Hoje armazenados, não aplicados (§15). |
Modelo de dados — schema platform no PostgreSQL
| Modelo Prisma | Tabela | Propósito | Campos-chave |
|---|---|---|---|
DecisionPlatformConfig | platform.decision_platform_configs | A esteira | key único por organização, decisionId, decisionKey, status, defaultTimeoutMs, maxTimeoutMs, callbackUrl, callbackHeaders |
PlatformConfigVersion | platform.platform_config_versions | Versão da esteira | version único por config, status, configSnapshot (JSONB), publishedAt |
PlatformDataSource | platform.platform_data_sources | Fonte de dados | name único por config, type, typeConfig (JSONB), inputMapping (JSONB), priority, isRequired, timeoutMs |
PlatformExecution | platform.platform_executions | Execução | input, mergedInput, output, error, decisionVersionId, decisionVersionNumber, trace, executionTimeMs, dataFetchTimeMs, dmnExecutionMs, callbackAttempts, traceId |
OrganizationPlatformConfig | platform.organization_platform_configs | Limites da organização | maxConcurrentExecutions, maxRetries, retryBackoffMs, retryBackoffMultiplier, executionRetentionDays |
Enumerações
| Enum | Valores |
|---|---|
PlatformConfigStatus | DRAFT · PUBLISHED · ARCHIVED |
DataSourceType | S3 · REDIS_STREAM · HTTP · BUREAU |
PlatformExecutionMode | SYNC · ASYNC |
PlatformExecutionStatus | PENDING · RUNNING · COMPLETED · FAILED · TIMEOUT · CANCELLED |
Tipos de fonte e o que cada uma aceita
| Tipo | Configuração (typeConfig) | Restrição de isolamento |
|---|---|---|
HTTP | url, method (GET/POST), headers, body, pagination | secureFetch bloqueia destino interno (anti-SSRF) |
S3 | bucket, key, format (JSON/PARQUET), region | key precisa começar com {organizationId}/ ou shared/ |
REDIS_STREAM | streamName, consumerGroup, consumerName, count, startId | streamName 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
transform | O que faz |
|---|---|
NONE | Usa o valor como veio (padrão) |
FLATTEN | Achata arrays aninhados |
FIRST | Pega o primeiro elemento do array |
LAST | Pega 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 noteAtençã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"
}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étodo | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /decision-platform/api/v1/configs | Cria esteira em DRAFT | DECISION_VERSIONS_CREATE |
GET | /decision-platform/api/v1/configs | Lista esteiras, paginado | DECISION_VERSIONS_READ |
GET | /decision-platform/api/v1/configs/:configId | Busca esteira | DECISION_VERSIONS_READ |
GET | /decision-platform/api/v1/configs/:configId/full | Esteira com versões e fontes | DECISION_VERSIONS_READ |
PATCH | /decision-platform/api/v1/configs/:configId | Atualiza nome, decisionKey, timeouts e callback | DECISION_VERSIONS_CREATE |
POST | /decision-platform/api/v1/configs/:configId/publish | Publica a esteira | DECISION_VERSIONS_CREATE |
DELETE | /decision-platform/api/v1/configs/:configId | Exclusão lógica. Responde 204 | DECISION_PROJECTS_DELETE |
Filtros de listagem: filter[status] (DRAFT, PUBLISHED, ARCHIVED), filter[decisionId] (UUID).
Repare que publicar a esteira exige
DECISION_VERSIONS_CREATEe nãoDECISION_VERSIONS_PUBLISH, e que excluir exigeDECISION_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étodo | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /decision-platform/api/v1/configs/:configId/versions | Cria versão, congelando o configSnapshot | DECISION_VERSIONS_CREATE |
GET | /decision-platform/api/v1/configs/:configId/versions | Lista versões da esteira | DECISION_VERSIONS_READ |
GET | /decision-platform/api/v1/configs/:configId/versions/latest | Última versão publicada | DECISION_VERSIONS_READ |
GET | /decision-platform/api/v1/versions/:versionId | Busca versão | DECISION_VERSIONS_READ |
POST | /decision-platform/api/v1/versions/:versionId/publish | Publica a versão | DECISION_VERSIONS_PUBLISH |
POST | /decision-platform/api/v1/versions/:versionId/archive | Arquiva a versão | DECISION_VERSIONS_CREATE |
Filtro: filter[status]. Só versão PUBLISHED pode ser arquivada — arquivar DRAFT retorna 400.
Fontes de dados — /decision-platform/api/v1
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /decision-platform/api/v1/configs/:configId/data-sources | Cria fonte na esteira | DECISION_VERSIONS_CREATE |
GET | /decision-platform/api/v1/configs/:configId/data-sources | Lista fontes da esteira | DECISION_VERSIONS_READ |
GET | /decision-platform/api/v1/data-sources/:dataSourceId | Busca fonte | DECISION_VERSIONS_READ |
PATCH | /decision-platform/api/v1/data-sources/:dataSourceId | Atualiza fonte | DECISION_VERSIONS_UPDATE |
DELETE | /decision-platform/api/v1/data-sources/:dataSourceId | Exclusão lógica. Responde 204 | DECISION_VERSIONS_DELETE |
Filtro de listagem: filter[type] (S3, REDIS_STREAM, HTTP).
Execução — /decision-platform/api/v1
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /decision-platform/api/v1/execute | Executa a esteira. 200 em SYNC, 202 em ASYNC | DECISIONS_EXECUTE |
GET | /decision-platform/api/v1/executions | Lista execuções, paginado | DECISIONS_READ |
GET | /decision-platform/api/v1/executions/:executionId | Execução completa, com mergedInput | DECISIONS_READ |
GET | /decision-platform/api/v1/executions/:executionId/status | Só status, datas e erro | DECISIONS_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étodo | Rota | Descrição | Permissão |
|---|---|---|---|
GET | /decision-platform/api/v1/org-config | Limites atuais. Devolve os padrões com isDefault: true se nunca configurado | DECISIONS_READ |
PUT | /decision-platform/api/v1/org-config | Substitui os limites | DECISIONS_MANAGE |
PATCH | /decision-platform/api/v1/org-config | Atualiza campos avulsos | DECISIONS_MANAGE |
DELETE | /decision-platform/api/v1/org-config | Volta aos padrões. Responde 204 | DECISIONS_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
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étodo | Rota | Descrição | Permissão |
|---|---|---|---|
GET | /decision-platform/api/v1/usage | Consumo do período, para cobrar e para a tela do cliente | DECISIONS_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.
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étodo | Rota | Descrição |
|---|---|---|
GET | /decision-platform/health | Sonda de vida do processo, com versão do build. Pública. |
POST /decision-platform/api/v1/execute
O endpoint que importa. Resolve a esteira pela chave, busca os dados, aplica a política e devolve.
Request
{
"configKey": "analise-credito-pessoal",
"mode": "SYNC",
"input": { "cpf": "12345678901", "valorSolicitado": 10000 },
"traceId": "proposta-2026-08-16-00042",
"timeoutMs": 15000
}{
"configKey": "analise-credito-pessoal",
"mode": "SYNC",
"input": { "cpf": "12345678901", "valorSolicitado": 10000 },
"traceId": "proposta-2026-08-16-00042",
"timeoutMs": 15000
}| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
configKey | string (1–100) | Sim | Chave da esteira. Não é o UUID. |
mode | SYNC | ASYNC | Não | Padrão SYNC. Sobre ASYNC, leia §15 |
input | object | Não | Padrão {}. Mesclado por cima dos dados das fontes |
versionId | string (UUID) | Não | Fixa uma versão da esteira. Precisa estar PUBLISHED e ser desta config |
callbackUrl | string (URL) | Não | Sobrepõe o callback da config. Só usado em ASYNC |
callbackHeaders | object | Não | Cabeçalhos extras do callback |
timeoutMs | int 1000–300000 | Não | Sobrepõe o defaultTimeoutMs da config |
traceId | string (≤255) | Não | Sua correlação. Indexado |
Resposta 200 — modo SYNC
{
"data": {
"type": "platform-execution",
"id": "9c2f...",
"attributes": {
"status": "COMPLETED",
"mode": "SYNC",
"output": { "decisao": "APROVADO", "limite": 8000 },
"timing": { "totalMs": 412, "dataFetchMs": 366, "dmnExecutionMs": 41 }
}
},
"links": { "self": "/api/v1/executions/9c2f..." }
}{
"data": {
"type": "platform-execution",
"id": "9c2f...",
"attributes": {
"status": "COMPLETED",
"mode": "SYNC",
"output": { "decisao": "APROVADO", "limite": 8000 },
"timing": { "totalMs": 412, "dataFetchMs": 366, "dmnExecutionMs": 41 }
}
},
"links": { "self": "/api/v1/executions/9c2f..." }
}O timing é o produto em três números: quanto tempo total, quanto foi buscar dado, quanto foi avaliar a regra.
Resposta 202 — modo ASYNC
{
"data": {
"type": "platform-execution",
"id": "9c2f...",
"attributes": { "status": "PENDING", "mode": "ASYNC", "callbackUrl": "https://...",
"traceId": "proposta-2026-08-16-00042" }
},
"links": { "self": "/api/v1/executions/9c2f..." }
}{
"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
| Status | Quando |
|---|---|
400 | No published version found for this config — a esteira não tem versão publicada |
400 | Version must be published to execute — o versionId informado não está PUBLISHED |
400 | Required data source '<nome>' failed: ... — uma fonte obrigatória não respondeu |
400 | No decision linked to this platform config — o configSnapshot não tem decisionId |
400 | No published decision version found for decision — a decisão no Decision Engine não tem versão publicada |
400 | DMN execution failed: ... — o runner recusou o XML ou a entrada mesclada |
403 | Token sem organizationId ou sem DECISIONS_EXECUTE |
404 | Platform config — não existe esteira com essa chave nesta organização |
Quando a execução falha depois de criada, a linha em platform_executions fica com status: FAILED e a mensagem em error. O executionId do erro não é devolvido no corpo — encontre a execução por filter[traceId] ou pelo intervalo de tempo.
POST /decision-platform/api/v1/configs/:configId/data-sources
Declara uma fonte de dados na esteira. Exemplo de um bureau por HTTP.
Request
{
"name": "bureau-principal",
"type": "HTTP",
"typeConfig": {
"url": "https://api.bureau-exemplo.com.br/v1/score",
"method": "POST",
"headers": { "Authorization": "Bearer ..." },
"body": { "documento": "PLACEHOLDER" }
},
"inputMapping": [
{ "path": "$.score", "targetField": "score", "transform": "NONE" },
{ "path": "$.restricoes[*]", "targetField": "restricoes", "transform": "FLATTEN" },
{ "path": "$.renda.estimada", "targetField": "rendaEstimada", "defaultValue": 0 }
],
"priority": 0,
"isRequired": true,
"timeoutMs": 3000
}{
"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
}| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
name | string (1–100) | Sim | Único dentro da esteira |
type | S3 | REDIS_STREAM | HTTP | Sim | Determina o schema de typeConfig |
typeConfig | object | Sim | Validado conforme o tipo; erro traz o motivo |
inputMapping | array (mín. 1) | Sim | path, targetField, transform, defaultValue |
priority | int 0–100 | Não | Padrão 0. Ordem de mesclagem, não de execução |
isRequired | boolean | Não | Padrão false |
timeoutMs | int 1000–300000 | Não | Padrão 5000 |
Erros
| Status | Quando |
|---|---|
400 | Invalid typeConfig for <TIPO>: ... — a configuração não bate com o tipo |
409 | Já existe fonte com esse nome nesta esteira |
A
typeConfigé gravada como JSONB sem criptografia. Não coloque segredo de longa duração emheaders— ver §14.
GET /decision-platform/api/v1/executions/:executionId
Devolve a execução completa. O campo que justifica a existência do endpoint é o mergedInput.
{
"data": {
"type": "platform-execution",
"id": "9c2f...",
"attributes": {
"configId": "...", "versionId": "...", "mode": "SYNC", "status": "COMPLETED",
"input": { "cpf": "12345678901", "valorSolicitado": 10000 },
"mergedInput": { "score": 730, "restricoes": [], "rendaEstimada": 5200,
"cpf": "12345678901", "valorSolicitado": 10000 },
"output": { "decisao": "APROVADO", "limite": 8000 },
"error": null,
"timing": { "totalMs": 412, "dataFetchMs": 366, "dmnExecutionMs": 41 },
"callbackAttempts": 0, "traceId": "proposta-2026-08-16-00042",
"executedBy": "...", "startedAt": "...", "completedAt": "..."
}
}
}{
"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.
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
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/v1TOKEN=$(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/v12. Descobrir a decisão que a esteira vai aplicar
DECISION_ID=$(curl -s "$ENGINE/decisions" -H "Authorization: Bearer $TOKEN" \
| jq -r '.data[] | select(.attributes.key=="checagem-idade") | .id')DECISION_ID=$(curl -s "$ENGINE/decisions" -H "Authorization: Bearer $TOKEN" \
| jq -r '.data[] | select(.attributes.key=="checagem-idade") | .id')Ela precisa estar ACTIVE e ter uma versão PUBLISHED, senão a esteira falha no passo 7.
3. Criar a esteira
CONFIG_ID=$(curl -s -X POST "$BASE/configs" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d "{
\"decisionId\": \"$DECISION_ID\",
\"name\": \"Análise simples\",
\"key\": \"analise-simples\",
\"defaultTimeoutMs\": 15000,
\"maxTimeoutMs\": 30000
}" | jq -r '.data.id')
echo $CONFIG_ID
# 4f7a1c62-... — a esteira nasce em DRAFTCONFIG_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 DRAFTA 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)
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
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
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
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"
}' | jqcurl -s -X POST "$BASE/execute" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"configKey": "analise-simples",
"mode": "SYNC",
"input": { "age": 25 },
"traceId": "teste-inicio-rapido-1"
}' | jq{
"data": {
"type": "platform-execution",
"id": "9c2f...",
"attributes": {
"status": "COMPLETED",
"mode": "SYNC",
"output": { "result": "adult" },
"timing": { "totalMs": 68, "dataFetchMs": 21, "dmnExecutionMs": 39 }
}
}
}{
"data": {
"type": "platform-execution",
"id": "9c2f...",
"attributes": {
"status": "COMPLETED",
"mode": "SYNC",
"output": { "result": "adult" },
"timing": { "totalMs": 68, "dataFetchMs": 21, "dmnExecutionMs": 39 }
}
}
}8. Ver o que a regra realmente recebeu
curl -s "$BASE/executions" -H "Authorization: Bearer $TOKEN" \
| jq '.data[0].attributes | {input, mergedInput, timing}'curl -s "$BASE/executions" -H "Authorization: Bearer $TOKEN" \
| jq '.data[0].attributes | {input, mergedInput, timing}'{
"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.
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
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
}'{ "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
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
}'{ "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
organizationIdou comshared/. Qualquer outro prefixo é recusado na validação, com mensagem explícita. O mesmo vale para o nome do stream Redis, com{organizationId}:oushared:. - A fonte opcional que falha some sem deixar rastro na execução. Escreva a tabela DMN com
defaultValuee uma linha que trate a ausência do campo — não presuma que ele sempre chega. prioritynão é ordem de execução. Todas as fontes saem em paralelo. Ele só decide quem sobrescreve quem na mesclagem, e a fonte comprioritymaior é 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
# 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
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}'{ "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
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
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
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.
# 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.
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 Engine | Decision Platform | |
|---|---|---|
| O que ele guarda | A regra: XML DMN versionado | A esteira: quais fontes, timeouts, callback |
| Quem escreve | Analista de risco, em tabela DMN | Engenharia, em configuração |
| De onde vêm os dados | Do chamador, no corpo da requisição | Das fontes declaradas, mais o corpo |
| Chamada típica | POST /decisions/:id/execute | POST /execute com configKey |
| Tem regras próprias? | Sim, é a razão de existir | Não. Sempre chama o Engine |
| Modo assíncrono | Não | Modelado; não processa hoje (§15) |
| Use quando | Você já tem todos os dados | Falta 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 block | Como se relaciona | Obrigatório |
|---|---|---|
| Decision Engine | Fornece a política. A esteira lê a última versão publicada da decisão apontada | Sim |
| IAM | Emite o token; todas as rotas exigem organizationId e permissão | Sim |
| File Storage | Compartilha a infraestrutura S3 que a fonte do tipo S3 lê | Não |
| Webhooks Engine | Fornece o secureFetch anti-SSRF usado no callback; pode entregar os eventos decision-platform.* | Não |
| Pricing Engine | Não é chamado pela esteira. Entre como fonte HTTP, ou depois da decisão | Não |
| Calculations Engine | Não é chamado pela esteira. Amortização, IOF e CET depois que a decisão aprovou | Não |
| Audit Trail | Registra quem publicou qual esteira; a execução é registrada aqui mesmo | Nã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" .-> F1O 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.
Configuração e operação
Variáveis de ambiente
| Variável | Descrição | Obrigatória | Padrão |
|---|---|---|---|
DATABASE_URL | PostgreSQL. Precisa alcançar os schemas platform e decisions | Sim | — |
REDIS_URL | Redis. Fila assíncrona e fontes do tipo REDIS_STREAM | Sim | — |
DMN_ENGINE_URL | Runner DMN. A Platform o alcança diretamente, não via serviço decision-engine | Sim (na prática) | http://localhost:8080 |
JWT_SECRET | Segredo HS256 do IAM, mínimo 44 caracteres | Sim | — |
S3_ENDPOINT, S3_BUCKET, S3_REGION | Acesso S3 para fontes do tipo S3 | Se usar S3 | — |
S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY | Credenciais S3. Ambas ou nenhuma — configuração parcial derruba o boot | Se usar S3 | — |
S3_FORCE_PATH_STYLE | Necessário com MinIO | Não | — |
PORT | Porta no modo standalone | Não | 3000 (mapeada para 3011) |
DEPLOYMENT_MODE | monolith ou standalone | Não | monolith |
MODULE_SELF | Identificação nos health checks | Não | decision-platform |
MODULE_DECISION_ENGINE_URLaparece 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 peloDmnProxyService.
Dependências de infraestrutura
| Dependência | Para quê |
|---|---|
| PostgreSQL | Schema platform, mais leitura do schema decisions do Decision Engine |
| Redis | Stream decision-platform:executions e fontes do tipo REDIS_STREAM |
| S3 (ou MinIO) | Fontes do tipo S3 |
| Runner DMN | Avaliação da política |
| IAM | Emissã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
| Limite | Valor |
|---|---|
defaultTimeoutMs e maxTimeoutMs da esteira | 1000 a 300000 ms; padrão 30000 |
timeoutMs da fonte | 1000 a 300000 ms; padrão 5000 |
priority da fonte | 0 a 100 |
| Nome da fonte | 100 caracteres, único por esteira |
| Chave da esteira | 100 caracteres, ^[a-z0-9-]+$, única por organização |
| Versão | Semver estrito, única por esteira |
traceId | 255 caracteres |
| Tentativas de callback | 3, com esperas de 1 s, 5 s e 15 s |
| Timeout do callback | 10000 ms, fixo em código |
| Página padrão / máxima | 20 / 100 itens |
maxConcurrentExecutions (org) | Padrão 100 — não aplicado (§15) |
executionRetentionDays (org) | Padrão 30 — não aplicado (§15) |
Catálogo de erros
| Status | Código | Significa | O que fazer |
|---|---|---|---|
400 | VALIDATION | Corpo reprovado no Zod | Confira tipos e faixas contra §9 |
400 | VALIDATION | Invalid typeConfig for <TIPO> | A configuração não bate com o tipo da fonte |
400 | VALIDATION | No published version found for this config | Crie e publique uma versão da esteira |
400 | VALIDATION | Required data source '<nome>' failed | A fonte obrigatória caiu ou estourou o timeout |
400 | VALIDATION | No published decision version found | A decisão no Decision Engine não tem versão publicada |
400 | VALIDATION | DMN execution failed | Confira se as chaves do mergedInput casam com a tabela |
400 | VALIDATION | SSRF protection: ... | O destino HTTP ou de callback é interno e foi bloqueado |
400 | VALIDATION | S3 key must be under organization prefix | Prefixe a chave com {organizationId}/ ou shared/ |
400 | VALIDATION | PARQUET format is not yet supported | Use JSON |
403 | — | Organization context required | Autentique informando a organização |
403 | FORBIDDEN | Permissão faltando, ou recurso de outra organização | Confira a permissão exata em §9 |
404 | NOT_FOUND | Platform config — chave inexistente nesta organização | Confira o configKey |
408 | TIMEOUT | Fonte HTTP ou S3 estourou o timeoutMs | Aumente o timeout, ou marque a fonte como opcional |
409 | CONFLICT | Chave, versão ou nome de fonte em uso | Escolha outro |
500 | INTERNAL | Falha ao buscar fontes, ou falha ao publicar evento | Verifique Redis, S3 e o destino HTTP |
Observabilidade
GET /decision-platform/healthresponde 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,callbackAttemptsecallbackLastError. 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}edecision-platform.org-config.updated. Os eventos detimeoutecancelledestão declarados e nenhum caminho de código os emite. - Monitore o tamanho do stream
decision-platform:executionsno Redis. Enquanto §15 valer, ele só cresce.
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 que | Regra |
|---|---|
| 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 nome | A 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 /execute | O 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ítica | Uma 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ção | Gravado com o dono da esteira, não com o de quem chamou. O consumo pertence a quem é dono. |
| Eventos e webhooks | Todo 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ção | A 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. |
| Cota | 402 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 novo | O 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 conta | Decisã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 fonte | Esteira 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
keyprecisa começar com{organizationId}/oushared/. Qualquer outro prefixo é recusado com400. Uma organização não consegue configurar leitura do prefixo de outra. - Redis Stream: o
streamNameprecisa começar com{organizationId}:oushared:, com a mesma recusa. - HTTP: passa pelo
secureFetchdo Webhooks Engine, que bloqueia destinos internos. Sem isso, uma fonte apontando parahttp://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.
Limitações conhecidas
| Limitação | Impacto | Situação |
|---|---|---|
O modo ASYNC não é processado | O 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 aplicados | maxConcurrentExecutions, 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 criptografados | typeConfig 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ítica | O 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 versionadas | Elas pertencem à config. Adicionar, alterar ou remover afeta todas as versões publicadas imediatamente. | Por design |
| Falha de fonte opcional não fica registrada | A execução não guarda quais fontes falharam nem por quê. Só o dataFetchTimeMs agregado sobrevive. | Lacuna conhecida |
| Uma decisão por esteira | Cada 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ídos | Estã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 S3 | Está no schema de configuração e a leitura retorna erro de validação. | Modelado, não implementado |
retryPendingCallbacks não tem quem chame | O 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 vazio | O arquivo contém apenas export {} e um comentário de tarefa pendente. | Não implementado |
Resolvido. 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 nenhuma | Responde 200 com banco, Redis, S3 e runner DMN fora. | Lacuna conhecida |
| Sem interface visual da esteira | Toda a configuração é por API. Não há tela para o analista montar ou testar a esteira. | Roadmap |
| Sem simulação de esteira | Não há como rodar uma esteira contra um lote de casos e comparar com a anterior. | Roadmap |
| Sem cache de fonte de dados | Toda 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 Engine | A 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 recurso | Publicar esteira exige DECISION_VERSIONS_CREATE; excluir exige DECISION_PROJECTS_DELETE. | Conceda papéis pela tabela de §9 |
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