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
01Resumo executivo
O Decision Platform é a esteira. Você faz uma chamada com o que tem em mãos — um CPF, um valor pedido —, e ela cuida do resto: busca os dados que faltam nas fontes que você declarou, junta tudo com o que você mandou, aplica a política de crédito vigente e devolve a decisão. Uma chamada entra, um veredito sai.
Na prática, ela substitui aquele serviço que quase toda fintech de crédito acaba escrevendo: o que só existe para chamar o bureau, esperar a resposta, tratar o timeout, juntar o JSON e então chamar a regra. Esse serviço nunca é o produto da empresa, mas consome time de engenharia todo mês e é onde os incidentes de esteira nascem. Aqui ele vira configuração: você declara as fontes, aponta para a política e chama POST /decision-platform/api/v1/execute.
A política em si não mora aqui. Ela mora no Decision Engine, como tabela DMN versionada. Essa separação é deliberada e é o assunto da §12.
Está em beta desde abril de 2026 — o modo síncrono está completo e o assíncrono ainda não processa (§15).
| 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 |
02O problemanegócio
O cenário. Uma fintech de crédito recebe uma proposta com CPF e valor. Para decidir, ela precisa de coisas que não vieram na proposta: score de bureau, histórico interno, restrições de uma lista, limites da carteira. Só depois de reunir isso é que a política pode ser aplicada.
O que trava hoje.
- A cola vira um serviço, e o serviço vira um problema. Buscar bureau, tratar timeout, tentar de novo, juntar o JSON e chamar a regra é código que nunca é o produto da empresa e que ninguém quer manter. Mesmo assim ele cresce a cada fonte nova.
- Cada fonte de dados é um projeto. Adicionar um bureau significa código novo, deploy, tratamento de erro específico e uma nova forma de a esteira quebrar. Numa esteira com quatro fontes, são quatro maneiras diferentes de falhar.
- Timeout é decisão de negócio tratada como detalhe técnico. Se o bureau está lento, a esteira deve esperar ou decidir sem ele? A resposta muda por fonte e por produto, mas costuma estar enterrada num
axios.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 custo de não resolver. Some o custo direto — o time que mantém a cola, mês a mês, sem entregar produto — com o indireto: cada incidente de esteira é proposta parada, e proposta parada em originação digital é conversão perdida. E há o custo de não saber: sem o tempo de cada etapa medido, a discussão sobre latência da análise vira opinião, e a decisão de trocar de bureau é tomada sem dado.
03Proposta de valornegócio
| 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.
O tempo vem separado. Toda execução devolve timing com totalMs, dataFetchMs e dmnExecutionMs. Você sabe se a lentidão está no bureau ou na regra sem instrumentar nada.
Toda execução fica registrada. A tabela platform_executions guarda a entrada original, a entrada já enriquecida com as fontes, a saída, o erro, os tempos e um traceId seu para correlação. Reproduzir uma análise não depende de log de aplicação.
A regra continua portátil. A política é uma tabela DMN da OMG que vive no Decision Engine e sai de lá por API. Trocar de orquestrador não obriga a reescrever a política — o que não é verdade em plataforma de formato proprietário.
04Casos de uso reaisnegócio
Caso 1 — Uma esteira de crédito que enriquece a proposta antes de decidir Cenário ilustrativo
Contexto. Fintech de crédito pessoal com originação digital. A proposta chega com CPF, valor pedido e prazo. Para decidir, faltam score de bureau e histórico interno do cliente.
A dor. A cola era um serviço Node de uns 2 mil linhas: chamava o bureau, tratava timeout, tinha um retry que ninguém lembrava por que era três, montava o objeto e chamava a regra. Toda fonte nova mexia nesse serviço. Quando a análise ficava lenta, ninguém sabia dizer se era o bureau ou a regra, porque o log só media o total.
A solução com o BB. Uma DecisionPlatformConfig chamada analise-credito-pessoal aponta para a decisão politica-credito-pessoal do Decision Engine. Duas fontes são declaradas por POST /decision-platform/api/v1/configs/:configId/data-sources: uma HTTP para o endpoint do bureau, com isRequired: true, timeoutMs: 3000 e priority: 0; outra S3 para o arquivo diário de histórico interno, com isRequired: false e priority: 10. O inputMapping de cada uma extrai os campos por caminho — $.score, $.data.comprometimento — e os renomeia para os nomes que a tabela DMN espera. Publicar a config e a versão liga a esteira. O aplicativo chama POST /decision-platform/api/v1/execute com {"configKey":"analise-credito-pessoal","input":{"cpf":"...","valor":10000}}.
O resultado. O serviço de cola some. Fonte nova vira chamada de API. E timing.dataFetchMs contra timing.dmnExecutionMs responde, em toda execução, onde o tempo foi.
Caso 2 — Um bureau secundário cai e a esteira continua de pé Cenário ilustrativo
Contexto. A mesma esteira, agora com três fontes: bureau principal, bureau secundário de enriquecimento e arquivo interno.
A dor. Na versão caseira, qualquer fonte que passasse do timeout derrubava a análise inteira, porque o Promise.all não distinguia essencial de complementar. Uma instabilidade de vinte minutos no bureau secundário virava vinte minutos de propostas recusadas por erro técnico — o pior tipo de recusa, porque o cliente não volta.
A solução com o BB. O bureau principal fica isRequired: true. O secundário e o arquivo ficam isRequired: false. As fontes são buscadas em paralelo; quando uma opcional falha, o DataSourceService simplesmente não a inclui nos dados mesclados e segue. A tabela DMN é escrita já contando com isso: se o campo do bureau secundário não chegou, a linha que depende dele não casa e a decisão cai numa faixa mais conservadora — que é uma decisão de negócio, tomada na tabela, e não um erro técnico.
O resultado. A queda de uma fonte complementar vira uma decisão mais conservadora em vez de uma falha. E quem define o que é essencial é quem configura a esteira, não quem escreveu o cliente HTTP.
Caso 3 — Reconstruir uma análise seis meses depois Cenário ilustrativo
Contexto. Operação regulada precisa demonstrar como uma proposta específica foi analisada.
A dor. Reconstruir exigia juntar log de aplicação, log do bureau e o código da regra da época — três fontes com retenções diferentes, e nenhuma delas guardando o dado exato que a regra recebeu depois do enriquecimento.
A solução com o BB. GET /decision-platform/api/v1/executions?filter[configId]=...&filter[from]=... acha a execução. GET /decision-platform/api/v1/executions/:executionId devolve input (o que o chamador mandou), mergedInput (o que a regra de fato recebeu, já com os dados das fontes), output, timing e traceId. Do lado da regra, o versionId do Decision Engine leva ao XML DMN que rodou.
O resultado. A entrada enriquecida é o campo que faz a diferença: sem ele, saber o que o bureau respondeu naquele instante seria impossível. Com ele, a análise é reproduzível.
Caso 4 — Por que o mercado cobra caro por isso Referência de mercado
Contexto. Orquestração de decisão virou uma categoria própria e bem capitalizada. A Taktile anunciou uma Série C de US$ 110 milhões liderada pelo Goldman Sachs e lista Allianz, Monzo, Mercury e Kueski entre os clientes. A Provenir informa mais de 120 parceiros de dados, presença em mais de 60 países — com escritório em São Paulo — e mais de 4 bilhões de decisões processadas por ano. A Alloy informa mais de 900 instituições financeiras e mais de 270 integrações de dados.
A dor do mercado. Nenhuma dessas empresas publica preço. As três vendem por cotação, com ciclo de venda corporativo. Para uma fintech em estágio inicial, a orquestração de decisão fica economicamente inacessível na fase em que ela é mais necessária — e a saída é escrever a cola em casa, que é exatamente o problema do §2. (Todos os números acima são declarações públicas dos próprios fornecedores, consultadas em 2026-08-16.)
Como a Catalisa endereça. O Decision Platform entra como um building block dentro de uma plataforma que o cliente já contrata, e não como uma plataforma nova a integrar. E o ativo que mais custa migrar — a política — fica em XML DMN padrão no Decision Engine, exportável por API. O valor não está em ter mais integrações que a Provenir: não temos. Está em a esteira ser proporcional ao tamanho de quem começa, e em o custo de sair dela ser conhecido no dia da assinatura.
05Mercado e diferenciaisnegócio
Panorama. A orquestração de decisão de crédito se organiza em três grupos. As plataformas globais de decisão — Provenir, Taktile, Alloy — vendem a esteira completa com marketplace de dados, autoria visual e governança, por cotação corporativa. Os fornecedores brasileiros de dados e decisão — a Trillia, ex-Neurotech, hoje um negócio da B3, a Serasa Experian, a Equifax — vendem a decisão acoplada aos próprios dados, o que é forte no Brasil e cria dependência do mesmo grupo. E há os fornecedores de modelagem — Zest AI — que resolvem a parte estatística e não resolvem orquestração.
O Decision Platform não compete no tamanho do marketplace de dados: qualquer uma das plataformas globais tem mais integrações prontas do que nós, e isso não vai mudar. Ele compete em outro eixo — ser a esteira de quem já usa a plataforma Catalisa, com a política em formato padrão e o custo de saída conhecido.
| Crité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, e conversa com Pricing Engine e Calculations Engine sem projeto de integração.
Quando escolher o concorrente. Se o que você precisa é não integrar bureau nenhum, a Provenir e a Alloy chegam com mais de cem integrações prontas e nós chegamos com zero — você traz o endpoint e a credencial, e isso é trabalho seu. Se o seu analista de risco precisa desenhar a esteira numa tela, arrastando etapas e testando cenários, a Taktile é hoje a melhor experiência de autoria do mercado e nós não temos interface. Se a sua operação depende profundamente de dados brasileiros — cadastro positivo, comportamento de mercado, modelos setoriais — a Trillia e a Serasa Experian vendem o dado e a decisão juntos, e essa integração vertical é uma vantagem real. Se você precisa de várias decisões encadeadas, com o resultado de uma alimentando a outra dentro da mesma esteira, nenhuma versão atual nossa faz isso: cada config aponta para exatamente uma decisão. E se você precisa de execução assíncrona em produção hoje, leia a §15 antes de qualquer coisa.
06Modelo de cobrança e ROInegócio
Unidade de cobrança. Precificação em definição. Quando definida, a unidade natural é a execução de esteira — uma decisão completa, do CPF ao veredito. É o que o cliente entende, e é o que a tabela platform_executions conta.
O que dispara custo.
| 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 |
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.
07Arquitetura
HTTP (Bearer JWT do IAM)
│
┌─────────────────────────────────┴────────────────────────────────────────┐
│ Hono app basePath('/decision-platform') │
│ │
│ /api/v1/configs configsRouter 7 rotas esteiras │
│ /api/v1 versionsRouter 6 rotas versões da esteira │
│ /api/v1 dataSourcesRouter 5 rotas fontes de dados │
│ /api/v1 executionsRouter 4 rotas execute + histórico │
│ /api/v1 orgConfigRouter 4 rotas limites da organização │
│ │
│ Toda rota: authMiddleware → requirePermission(P) → requireOrganization │
└─────────────────────────────────┬────────────────────────────────────────┘
│
┌─────────────────────────────────┴────────────────────────────────────────┐
│ ExecutionService │
│ executeSync → responde na hora │
│ executeAsync → enfileira no Redis Stream e responde 202 │
└───────┬──────────────────────────────────────────┬───────────────────────┘
│ │
┌───────┴────────────────┐ ┌────────────────┴────────────────────────┐
│ DataSourceService │ │ AsyncExecutionConsumer │
│ busca em PARALELO, │ │ grupo 'execution-processors' no stream │
│ ordena por priority │ │ 'decision-platform:executions' │
│ ┌──────────────────┐ │ │ ⚠ NÃO É INICIADO HOJE — ver §15 │
│ │ S3Handler │ │ └──────────────────┬───────────────────────┘
│ │ RedisHandler │ │ │
│ │ HttpHandler │ │ CallbackService
│ └──────────────────┘ │ (secureFetch, 3 tentativas)
└───────┬────────────────┘
│ mergedInput = { ...dadosDasFontes, ...inputDoChamador }
▼
┌───────────────────────────────────────────────────────────────────────────┐
│ Decision Engine (in-process) │
│ VersionRepository.findLatestPublished(decisionId) → XML DMN │
│ DmnProxyService.execute(dmn, mergedInput) ──HTTP──▶ runner DMN │
└───────────────────────────────────────────────────────────────────────────┘
A esteira ponta a ponta — o diagrama que descreve o produto:
┌──────────┐
│ Proposta │ POST /decision-platform/api/v1/execute
│ (CPF, │ { "configKey": "analise-credito", "input": {...} }
│ valor) │
└────┬─────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ 1. RESOLVE A ESTEIRA config pela chave → versão publicada mais recente │
└────┬────────────────────────────────────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ 2. BUSCA OS DADOS todas as fontes EM PARALELO, ordenadas por priority │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ HTTP │ │ S3 │ │ REDIS_STREAM│ │
│ │ bureau │ │ histórico │ │ eventos │ │
│ │ required ✓ │ │ opcional │ │ opcional │ │
│ │ 3000 ms │ │ 5000 ms │ │ 2000 ms │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
│ └─────────────────┴─────────────────┘ │
│ inputMapping por fonte: $.score → score │
│ required que falha → 400 · opcional que falha → segue │
└────┬────────────────────────────────────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ 3. JUNTA mergedInput = { ...dadosDasFontes, ...inputDoChamador } │
│ o input do chamador SOBRESCREVE o das fontes em colisão │
└────┬────────────────────────────────────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ 4. APLICA A POLÍTICA Decision Engine → última versão PUBLISHED da │
│ decisão apontada → runner DMN avalia a tabela │
└────┬────────────────────────────────────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ 5. REGISTRA PlatformExecution: input, mergedInput, output, traceId, │
│ timing { totalMs, dataFetchMs, dmnExecutionMs } │
└────┬────────────────────────────────────────────────────────────────────┘
▼
┌──────────────────┐ ┌──────────────────────────────────────────┐
│ SYNC: 200 com │ │ ASYNC: 202 na hora, callback depois │
│ a decisão │ │ ⚠ o processador não roda hoje — §15 │
└──────────────────┘ └──────────────────────────────────────────┘
Onde entram precificação e cálculo: o Pricing Engine e o Calculations Engine
NÃO são chamados pela esteira. Eles entram como fonte de dados do tipo HTTP,
ou depois, quando o seu aplicativo já tem a decisão em mãos. Ver §12.
Decisões não óbvias.
- O input do chamador vence o das fontes. A mesclagem é
{ ...mergedData, ...input.input }. Se o bureau 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. - A versão da esteira congela a configuração, não a regra. O
configSnapshotdaPlatformConfigVersionguarda nome, chave, timeouts,callbackUrledecisionIdno 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. - 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).
08Conceitos 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, 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, 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 |
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
Config e Versão Execução
─────────────── ────────
┌───────┐ POST /configs/:id/publish ┌─────────┐
│ DRAFT │ ─────────────────────────▶ │ PENDING │ criada (só em ASYNC)
└───────┘ └────┬────┘
│ A versão tem o MESMO ciclo, com │ o processador pega
│ POST /versions/:id/publish ▼
▼ ┌─────────┐
┌───────────┐ │ RUNNING │
│ PUBLISHED │ executa └────┬────┘
└─────┬─────┘ │
│ POST /versions/:id/archive ┌─────┴────────┬──────────┐
▼ ▼ ▼ ▼
┌──────────┐ ┌───────────┐ ┌────────┐ ┌─────────┐
│ ARCHIVED │ │ COMPLETED │ │ FAILED │ │ TIMEOUT │
└──────────┘ └───────────┘ └────────┘ └─────────┘
Para uma esteira executar, PRECISA de pelo menos uma PlatformConfigVersion
PUBLISHED. Sem ela: 400 "No published version found for this config".
Publicar a CONFIG não basta — são dois objetos e dois publishes distintos.
TIMEOUT e CANCELLED existem no enum e nenhum caminho de código os atribui.
Em SYNC a execução vai direto de RUNNING para COMPLETED ou FAILED.
09Referência da API
Prefixo: /decision-platform. Em staging, a base é https://decision-platform.bb.stg.catalisa.app.
Todas as rotas exigem: authMiddleware (Bearer JWT do IAM), o requirePermission indicado, e requireOrganization — token sem organizationId recebe 403.
Corpos aceitam JSON:API ({"data":{"attributes":{...}}}) ou o objeto direto, exceto POST /execute, que lê o corpo cru.
Esteiras (configs) — /decision-platform/api/v1/configs
| Mé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, 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.
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
}
| 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..." }
}
O timing é o produto em três números: quanto tempo total, quanto foi buscar dado, quanto foi avaliar a regra.
Resposta 202 — modo ASYNC
{
"data": {
"type": "platform-execution",
"id": "9c2f...",
"attributes": { "status": "PENDING", "mode": "ASYNC", "callbackUrl": "https://...",
"traceId": "proposta-2026-08-16-00042" }
},
"links": { "self": "/api/v1/executions/9c2f..." }
}
Aviso. A execução é criada e enfileirada no Redis Stream, mas nenhum processo consome essa fila na configuração atual. A execução permanece
PENDINGindefinidamente e o callback não é entregue. UseSYNCem 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
}
| 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": "..."
}
}
}
input é o que o chamador mandou; mergedInput é o que a regra recebeu. A diferença entre os dois é o que as fontes trouxeram.
10Início rápido
Uma esteira ponta a ponta, do zero à primeira decisão. Assume que você já criou a decisão no Decision Engine — se ainda não, siga primeiro o início rápido de lá.
1. Autenticar
TOKEN=$(curl -s -X POST https://iam.bb.stg.catalisa.app/iam/api/v1/users/login \
-H "Content-Type: application/json" \
-d '{
"email": "admin@test.com",
"password": "password123",
"organizationId": "b0000000-0000-0000-0000-000000000001"
}' | jq -r .accessToken)
BASE=https://decision-platform.bb.stg.catalisa.app/decision-platform/api/v1
ENGINE=https://decision-engine.bb.stg.catalisa.app/decision-engine/api/v1
2. Descobrir a decisão que a esteira vai aplicar
DECISION_ID=$(curl -s "$ENGINE/decisions" -H "Authorization: Bearer $TOKEN" \
| jq -r '.data[] | select(.attributes.key=="checagem-idade") | .id')
Ela precisa estar ACTIVE e ter uma versão PUBLISHED, senão a esteira falha no passo 7.
3. Criar a esteira
CONFIG_ID=$(curl -s -X POST "$BASE/configs" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d "{
\"decisionId\": \"$DECISION_ID\",
\"name\": \"Análise simples\",
\"key\": \"analise-simples\",
\"defaultTimeoutMs\": 15000,
\"maxTimeoutMs\": 30000
}" | jq -r '.data.id')
4. Declarar uma fonte de dados (opcional — a esteira funciona sem nenhuma)
curl -s -X POST "$BASE/configs/$CONFIG_ID/data-sources" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"name": "idade-externa",
"type": "HTTP",
"typeConfig": { "url": "https://api.exemplo.com.br/perfil", "method": "GET" },
"inputMapping": [ { "path": "$.idade", "targetField": "age", "defaultValue": 0 } ],
"priority": 0,
"isRequired": false,
"timeoutMs": 3000
}' | jq '.data.attributes.name'
O targetField é age porque é assim que a tabela DMN chama a variável. O mapeamento é a cola entre a fonte e a regra.
5. Criar e publicar a versão da esteira
VERSION_ID=$(curl -s -X POST "$BASE/configs/$CONFIG_ID/versions" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"version":"1.0.0"}' | jq -r '.data.id')
curl -s -X POST "$BASE/versions/$VERSION_ID/publish" \
-H "Authorization: Bearer $TOKEN" | jq '.data.attributes.status'
# "PUBLISHED"
6. Publicar a esteira
curl -s -X POST "$BASE/configs/$CONFIG_ID/publish" \
-H "Authorization: Bearer $TOKEN" | jq '.data.attributes.status'
# "PUBLISHED"
São dois objetos e dois publishes: a config e a versão. O que a execução exige é a versão publicada.
7. Executar a esteira
curl -s -X POST "$BASE/execute" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"configKey": "analise-simples",
"mode": "SYNC",
"input": { "age": 25 },
"traceId": "teste-inicio-rapido-1"
}' | jq
{
"data": {
"type": "platform-execution",
"id": "9c2f...",
"attributes": {
"status": "COMPLETED",
"mode": "SYNC",
"output": { "result": "adult" },
"timing": { "totalMs": 68, "dataFetchMs": 21, "dmnExecutionMs": 39 }
}
}
}
8. Ver o que a regra realmente recebeu
curl -s "$BASE/executions" -H "Authorization: Bearer $TOKEN" \
| jq '.data[0].attributes | {input, mergedInput, timing}'
Credenciais de staging, conforme AMBIENTES.md. Nunca use credencial de produção em documentação ou script de exemplo.
11Receitas
Montar uma esteira de crédito com bureau obrigatório e enriquecimento opcional
O padrão de esteira mais comum: uma fonte que não pode faltar e outras que são bônus.
# Bureau principal — sem ele não há decisão
curl -s -X POST "$BASE/configs/$CONFIG_ID/data-sources" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"name":"bureau-principal","type":"HTTP",
"typeConfig":{"url":"https://api.bureau.com.br/score","method":"POST",
"headers":{"Authorization":"Bearer TOKEN_DO_BUREAU"},
"body":{"documento":"placeholder"}},
"inputMapping":[{"path":"$.score","targetField":"score"},
{"path":"$.restricoes[*]","targetField":"restricoes","transform":"FLATTEN"}],
"priority":0,"isRequired":true,"timeoutMs":3000
}'
# Histórico interno em S3 — se faltar, a política decide mais conservadora
curl -s -X POST "$BASE/configs/$CONFIG_ID/data-sources" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"name":"historico-interno","type":"S3",
"typeConfig":{"bucket":"catalisa-dados",
"key":"b0000000-0000-0000-0000-000000000001/historico/2026-08-16.json",
"format":"JSON"},
"inputMapping":[{"path":"$.comprometimento","targetField":"comprometimento","defaultValue":0}],
"priority":10,"isRequired":false,"timeoutMs":5000
}'
Armadilhas.
- A chave do S3 precisa começar com o
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.
# Tudo acontece no Decision Engine
VERSION_ID=$(jq -Rs '{version:"1.1.0", content:.}' < politica-nova.dmn \
| curl -s -X POST "$ENGINE/decisions/$DECISION_ID/versions" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d @- \
| jq -r '.data.id')
curl -s -X POST "$ENGINE/versions/$VERSION_ID/publish" -H "Authorization: Bearer $TOKEN"
# A esteira já está usando a política nova. Nenhuma chamada ao decision-platform.
Armadilhas. Esta é a coisa mais importante do building block, e ela corta dos dois lados. A esteira sempre usa a última versão publicada da decisão — publicar no Decision Engine muda o comportamento de todas as esteiras que apontam para ela, na hora. Uma versão publicada da esteira não protege contra isso, porque o configSnapshot congela o decisionId, e não a versão do DMN. Se você precisa de duas esteiras com políticas diferentes, use duas decisões distintas no Decision Engine, não duas versões da mesma.
Reconstruir uma análise para auditoria
# 1. Achar pela sua correlação ou pelo intervalo
curl -s "$BASE/executions?filter[configId]=$CONFIG_ID&filter[from]=2026-08-01" \
-H "Authorization: Bearer $TOKEN" | jq '.data[] | {id, traceId: .attributes.traceId}'
# 2. A execução completa, com o que a regra recebeu
curl -s "$BASE/executions/$EXEC_ID" -H "Authorization: Bearer $TOKEN" \
| jq '.data.attributes | {input, mergedInput, output, timing, versionId}'
# 3. A REGRA que rodou, do lado do Decision Engine
curl -s "$ENGINE/decisions/$DECISION_ID/execution-logs?filter[from]=..." \
-H "Authorization: Bearer $TOKEN" | jq '.data[0].attributes.versionId'
Armadilhas. O versionId que a PlatformExecution guarda é o da versão da esteira, não o da versão do DMN. Para saber qual tabela rodou, é preciso cruzar pelo horário com o ExecutionLog do Decision Engine — não há ligação direta entre as duas execuções (§15). Guarde o mesmo traceId no seu lado, é o que torna esse cruzamento suportável.
Diagnosticar uma esteira lenta
curl -s "$BASE/executions?filter[status]=COMPLETED&page[size]=100" \
-H "Authorization: Bearer $TOKEN" \
| jq '[.data[].attributes.timing] | {
total_medio: (map(.totalMs) | add / length),
busca_media: (map(.dataFetchMs) | add / length),
regra_media: (map(.dmnExecutionMs) | add / length)
}'
Armadilhas. dataFetchMs é o tempo da fonte mais lenta, porque a busca é paralela — não é a soma. Se ele está alto, uma fonte está segurando todas: reduza o timeoutMs dela e a marque como opcional, e a esteira passa a degradar em vez de esperar. Se dmnExecutionMs está alto, o problema é o tamanho da tabela DMN ou a latência até o runner, e a investigação continua no Decision Engine.
Preparar-se para o modo assíncrono
Enquanto o processador não roda (§15), a alternativa em produção é assumir a assincronia do seu lado.
# Chame SYNC a partir da sua própria fila de trabalho, com o traceId da proposta
curl -s -X POST "$BASE/execute" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d "{\"configKey\":\"analise-credito\",\"mode\":\"SYNC\",
\"input\":{...},\"traceId\":\"$PROPOSTA_ID\",\"timeoutMs\":20000}"
Armadilhas. Dimensione o timeoutMs acima da soma esperada: ele precisa cobrir a fonte mais lenta mais a avaliação da regra. O teto é 300000 ms, mas manter uma conexão HTTP aberta por minutos é frágil — o caminho saudável é apertar o timeout das fontes. E não use mode: "ASYNC" esperando callback: a chamada responde 202 e a execução fica PENDING para sempre.
12Integração com outros building blocks
A fronteira que mais gera dúvida: Decision Platform ou Decision Engine?
Esta é a primeira pergunta de todo integrador, e a resposta é curta: o Decision Engine é a regra; o Decision Platform é a esteira que leva os dados até a regra.
| Decision 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 |
┌───────────────┐
│ Seu aplicativo│ POST /decision-platform/api/v1/execute
└───────┬───────┘
▼
┌─────────────────────────── Decision Platform ─────────────────────────────┐
│ │
│ fontes declaradas junta aplica a política │
│ ┌────────────┐ │
│ │ HTTP bureau│──┐ │
│ ├────────────┤ │ mergedInput ┌─────────────────────────┐ │
│ │ S3 arquivo │──┼────────────────────▶ │ Decision Engine │ │
│ ├────────────┤ │ │ última versão PUBLISHED│ │
│ │ Redis fila │──┘ │ da decisão apontada │ │
│ └────────────┘ └───────────┬─────────────┘ │
│ ▲ │ HTTP │
│ │ o Pricing Engine e o Calculations ▼ │
│ │ Engine entram AQUI, como fonte HTTP ┌──────────────┐ │
│ │ — não há integração dedicada │ runner DMN │ │
│ │ └──────────────┘ │
│ ┌────┴──────────┐ ┌──────────────────┐ │
│ │ Pricing Engine│ │Calculations Engine│ │
│ └───────────────┘ └──────────────────┘ │
└────────────────────────────────┬───────────────────────────────────────────┘
▼
┌────────────────────────┐
│ decisão + timing + │ ─────▶ Webhooks Engine
│ registro auditável │ (eventos da esteira)
└───────────┬────────────┘
▼
┌────────────────────────┐
│ APROVADO → precificar │ Pricing Engine
│ e calcular │ Calculations Engine
└────────────────────────┘
O argumento comercial em uma imagem: uma proposta entra, uma decisão sai,
e cada peça do caminho é um building block que o cliente já tem contratado.
A alternativa de mercado é integrar quatro fornecedores para desenhar isto.
Sobre precificação e cálculo, sem rodeio. A esteira não chama o Pricing Engine nem o Calculations Engine — não existe integração dedicada no código. Há dois caminhos honestos: expor o endpoint deles como uma fonte de dados do tipo HTTP, o que os traz para dentro da esteira antes da decisão; ou chamá-los do seu aplicativo depois que a decisão aprovou, que é a ordem natural — primeiro decide se empresta, depois a que taxa e em quantas parcelas.
13Configuração e operação
Variáveis de ambiente
| Variá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 |
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.
14Segurança e compliance
Isolamento entre tenants. Toda rota aplica o requireOrganization local, e o organizationId vem do claim assinado — nunca do corpo. DecisionPlatformConfig e PlatformExecution carregam a coluna organization_id. PlatformConfigVersion, PlatformDataSource e as execuções acessadas por ID são verificadas por junção com a config dona, comparando config.organizationId com o do token e respondendo 403 ou 404 quando diverge.
Isolamento nas fontes de dados — a parte que importa. As fontes leem de infraestrutura compartilhada, então o isolamento é validado na configuração, não presumido:
- S3: a
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.
O prefixo shared/ e shared: é um caminho deliberado de compartilhamento entre organizações. Ele é seguro na medida em que você controla o que coloca lá — trate esse prefixo como público dentro da instalação.
Callback. A callbackUrl é do cliente e aponta para fora, o que faz dela superfície de SSRF por definição. Ela sai pelo mesmo secureFetch, com timeout de 10 s e no máximo 3 tentativas. O corpo do callback contém a saída da decisão — se a política devolve limite ou motivo de recusa, esse dado trafega para o destino que o cliente configurou. Use HTTPS e um cabeçalho de autenticação em callbackHeaders.
Segredos nas fontes de dados. O typeConfig de uma fonte HTTP normalmente carrega um cabeçalho Authorization com a credencial do bureau. Esse JSONB é gravado sem criptografia e é devolvido pelas rotas de leitura da fonte a quem tiver DECISION_VERSIONS_READ. Mesma coisa com o callbackHeaders da config, que o código grava como JSON simples — o comentário no schema Prisma diz "Encrypted JSON", mas a criptografia não está implementada. Consequências práticas: use credencial de curta duração ou rotacionável quando possível, restrinja DECISION_VERSIONS_READ a quem precisa, e trate o schema platform como base que contém segredo de terceiros.
Dados pessoais nas execuções. platform_executions guarda input, mergedInput e output em JSONB. Numa esteira de crédito, o mergedInput é o registro mais sensível da plataforma: ele reúne, numa linha, o CPF que o cliente mandou e tudo o que o bureau respondeu. Não há criptografia em coluna, não há mascaramento e não há expurgo automático — o executionRetentionDays é armazenado e não aplicado (§15). Defina um processo de expurgo antes de entrar em produção com volume.
Explicabilidade. O art. 20 da LGPD assegura ao titular o direito de solicitar revisão de decisões automatizadas que afetem seus interesses, incluindo as que definem perfil de crédito. O par mergedInput mais output, cruzado com o ExecutionLog do Decision Engine, é a base factual para atender a esse pedido: o que entrou, o que a regra recebeu, qual tabela avaliou e o que saiu. O processo de revisão humana continua sendo seu.
Autenticação e permissões. Bearer JWT do IAM. As permissões usadas são DECISION_VERSIONS_{CREATE,READ,UPDATE,DELETE,PUBLISH}, DECISION_PROJECTS_DELETE, DECISIONS_EXECUTE, DECISIONS_READ e DECISIONS_MANAGE. Trate DECISIONS_EXECUTE como sensível: como o input do chamador sobrescreve os dados das fontes, quem executa pode fixar um score em vez de deixar o bureau respondê-lo.
15Limitações conhecidas
| Limitaçã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. | 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 |
| A execução não referencia a versão do DMN | PlatformExecution.versionId é a versão da esteira. Ligar a execução à versão da política exige cruzar por horário com o ExecutionLog do Decision Engine. | Lacuna conhecida — use traceId |
/health não verifica dependência 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 |
16Perguntas frequentes
Qual a diferença entre o Decision Platform e o Decision Engine?
O Engine é a regra; a Platform é a esteira que leva os dados até a regra. Se você já tem todos os dados de entrada em mãos, chame o Engine direto e pronto. Se alguém precisa consultar um bureau, ler um arquivo ou puxar de uma fila antes de decidir, é a Platform — e ela vai terminar chamando uma tabela DMN que vive no Engine de qualquer forma. A Platform não tem regras próprias e nunca terá: essa é a divisão.
Posso usar a Platform sem o Decision Engine?
Não. Toda esteira aponta para uma decisão do Engine, e a execução falha com No decision linked to this platform config se esse elo não existir. A Platform sem o Engine não teria o que executar.
Posso usar o Decision Engine sem a Platform?
Pode, e muita gente deve. Se a sua aplicação já tem os dados e só quer o veredito, o Engine sozinho resolve com menos peças móveis. A Platform só se paga quando existe busca de dado no meio do caminho.
Se eu publicar uma política nova no Engine, a esteira muda sozinha?
Muda, na hora. A esteira sempre usa a última versão publicada da decisão apontada. Publicar uma versão da esteira não protege contra isso, porque o snapshot congela qual decisão, e não qual versão dela. Para ter políticas diferentes em esteiras diferentes, crie decisões separadas no Engine.
Posso usar o modo assíncrono em produção?
Não hoje. A execução é enfileirada e nada a processa: ela fica PENDING para sempre e o callback não sai. Está registrado em §15 e é a razão de o building block estar em beta. Use SYNC a partir da sua própria fila.
O que acontece se o bureau cair no meio de uma análise?
Depende do isRequired daquela fonte. Se for true, a execução falha com Required data source '<nome>' failed e a linha fica FAILED. Se for false, a esteira segue sem os dados dela e a política decide com o que chegou — que é por isso que a tabela DMN deve ter uma linha tratando a ausência do campo.
Como sei se a lentidão é do bureau ou da regra?
Pelo timing que vem em toda resposta e fica gravado: dataFetchMs é a busca, dmnExecutionMs é a avaliação. Lembre que dataFetchMs é o tempo da fonte mais lenta, não a soma, porque a busca é paralela.
Posso colocar o Pricing Engine dentro da esteira?
Pode, como uma fonte de dados do tipo HTTP apontando para o endpoint dele. Não há integração dedicada. Na maioria dos desenhos, porém, a ordem natural é outra: primeiro a esteira decide se empresta, depois o seu aplicativo chama o Pricing Engine para saber a que taxa e o Calculations Engine para montar as parcelas.
Onde ficam guardadas as credenciais do meu bureau?
No typeConfig da fonte, como JSONB no schema platform, sem criptografia, e legíveis por quem tiver DECISION_VERSIONS_READ. Está em §14 e §15. Restrinja essa permissão e prefira credencial rotacionável.
Quanto tempo o histórico de execução é guardado?
Indefinidamente. O executionRetentionDays existe na configuração da organização, tem padrão de 30 dias, e não é aplicado por nenhum processo. Se você precisa de expurgo, hoje ele é seu.
A esteira funciona sem nenhuma fonte de dados?
Funciona. Sem fontes, o mergedInput é igual ao input e a esteira vira um invólucro em volta do Engine — que é o formato do início rápido, e é um jeito razoável de começar antes de plugar o primeiro bureau.
Padrão: PADRAO-DOCUMENTACAO.md · Índice: INDEX.md