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

Decision Engine

Produção

Regras de negócio em tabelas DMN, versionadas e alteradas sem deploy

25
Endpoints
6
Entidades
1
Provedores
Tenant
Escopo
3005
Porta
2026-03
Desde

A sua política de crédito para de ser um `if` escondido no código e vira uma tabela versionada que o analista de risco lê, revisa e publica. Mudar a regra deixa de ser um deploy e passa a ser uma publicação.

Para quem é
  • Fintechs e financeiras cuja política de crédito muda mais rápido que o ciclo de release do time
  • Seguradoras e operadoras de saúde com tabelas de aceitação e precificação que mudam por norma
  • Times de produto que precisam provar a um auditor qual regra rodou em qual data
Substitui
  • Licença de um BRMS comercial (IBM ODM, FICO Blaze Advisor) para o caso de tabelas de decisão
  • Operação própria de um cluster Drools ou Camunda só para avaliar regras
  • Blocos de `if/else` de política de negócio espalhados pelo código da aplicação
O que não é
  • Um orquestrador de esteira — ele não busca dado em bureau nem chama outro serviço (isso é o Decision Platform)
  • Um motor de workflow ou BPM com tarefas humanas, timers e compensação
  • Um modelo estatístico ou de machine learning; ele executa regras determinísticas que você escreve
O que dá para fazer

26 endpoints em 6 recursos.

Explorar a API →
01

Resumo executivo

O Decision Engine guarda as regras de negócio da sua empresa como tabelas de decisão em vez de código. Você descreve a política — "score acima de 700 e renda acima de 5 mil aprova até 20 mil" — como uma tabela com linhas e colunas, versiona essa tabela, publica a versão e chama uma API para obter a resposta.

Na prática isso muda quem manda na regra. Hoje, mudar um limite de crédito costuma significar abrir um chamado, esperar um desenvolvedor, esperar um code review e esperar a próxima janela de deploy. Com o Decision Engine, o analista de risco altera a tabela, cria a versão 1.4.0, publica, e a próxima requisição já usa a regra nova — sem que uma linha do seu aplicativo mude.

Está em produção desde março de 2026, é usado pelo Decision Platform como motor de avaliação e pelo Feature Flags para resolver pertencimento a segmentos.

AtributoValor
Identificadordecision-engine
CategoriaDecisão
EscopoTenant (exige organizationId no token)
Porta (standalone)3005
Path alias@decision-engine
Prefixo HTTP/decision-engine
Schema no bancodecisions
StatusProdução desde 2026-03
Depende dePostgreSQL, runner DMN (DMN_ENGINE_URL), IAM

02

O problema

negócio

O cenário. Uma financeira tem uma política de crédito. Essa política é um conjunto de faixas: score, renda, comprometimento, produto, canal. Ela muda o tempo todo — por decisão comercial, por pressão de inadimplência, por mudança de norma. E ela precisa ser explicável, porque alguém um dia vai perguntar por que aquele cliente foi negado.

O caminho que uma mudança de faixa percorre hoje, quando a política mora no código, é este:

flowchart LR
  subgraph Negocio["Quem entende a política"]
    A["Comitê de risco decide"]
  end
  subgraph Software["Quem controla o calendário"]
    B["Abre chamado"] --> C["Desenvolvedor traduz a regra em código"]
    C --> D["Code review"]
    D --> E["Fila de release"]
    E --> F["Janela de deploy"]
  end
  A --> B
  F --> G["A regra passa a valer"]

Nenhuma etapa entre a decisão e a janela de deploy tem a ver com risco de crédito. Todas elas são processo de software — e é o processo de software que define quando a política nova entra no ar.

O que trava hoje.

  • A regra vive no código, então quem a muda é o desenvolvedor. O analista de risco, que é quem entende a política, não consegue nem ler o if aninhado que a implementa. A informação passa por tradução, e tradução perde coisa.
  • Mudar a regra exige deploy. Um ajuste de faixa entra na fila de release junto com refatoração de banco e correção de bug. O tempo entre decidir e valer não é técnico, é de processo — e é medido em semanas.
  • Não dá para dizer qual regra rodou. Seis meses depois, com dez deploys no meio, responder "qual era a política no dia 12 de março" significa arqueologia de Git. Se o cálculo mudou de lugar, nem isso resolve.
  • Testar a política nova é caro. Sem separação entre a regra e o aplicativo, comparar a política nova contra a antiga exige subir dois ambientes.
  • Adotar um BRMS comercial troca um problema por outro. Motores corporativos resolvem a governança, mas trazem licença por cotação, ciclo de implantação longo e um formato de regra que só roda dentro deles.

O custo de não resolver. O custo direto é a política que continua errada enquanto espera a fila de deploy — cada dia de atraso em apertar um critério de risco é carteira originada com a regra antiga. O custo indireto é a auditoria: sem log de qual versão avaliou qual proposta, a resposta ao regulador vira reconstrução manual. E há um custo regulatório concreto, porque o art. 20 da LGPD garante ao titular o direito de solicitar revisão de decisões tomadas unicamente com base em tratamento automatizado, incluindo as que definem perfil de crédito — e revisar exige saber o que rodou.


03

Proposta de valor

negócio
AntesDepois
A regra é um if que só o desenvolvedor lêA regra é uma tabela DMN que o analista de risco lê
Mudar a política entra na fila de deployMudar a política é POST .../versions + POST .../publish
"Qual regra rodou naquele dia?" é arqueologia de GitToda execução grava versão, entrada, saída e tempo
A regra nova substitui a antiga e someVersões coexistem: rascunho, publicada e arquivada
Trocar de motor significa reescrever tudoO XML é DMN da OMG e roda em qualquer motor conforme

A regra sai do código. Uma tabela de decisão tem entradas, saídas e linhas. Quem escreve política de crédito já pensa assim — a tabela é o formato natural, não uma tradução dele.

Publicar é o gatilho, não o deploy. Uma versão nasce DRAFT e não executa. Ela só passa a valer quando alguém com a permissão DECISION_VERSIONS_PUBLISH a publica. A separação entre escrever e valer é o controle que uma política de crédito exige.

São dois interruptores independentes, e a regra só responde quando os dois estão ligados:

flowchart LR
  W["Analista escreve a tabela DMN"] --> V["Versão nasce DRAFT"]
  V -->|"publish"| P["Versão PUBLISHED"]
  D["Decisão nasce DRAFT"] -->|"PATCH status ACTIVE"| DA["Decisão ACTIVE"]
  P --> X{"Os dois ligados?"}
  DA --> X
  X -->|"sim"| OK["A regra responde em produção"]
  X -->|"não"| ERR["HTTP 400"]

Toda execução deixa rastro. Cada chamada grava um ExecutionLog com a entrada, a saída, o tempo em milissegundos, quem executou e — o que mais importa — qual versão foi usada. Responder "por que este cliente foi negado" vira uma consulta.

O DMN inválido não entra. Ao criar uma versão, o serviço valida o XML contra o runner DMN antes de gravar. Você descobre o erro na hora de subir a regra, não na primeira proposta real.

Atenção. A validação acontece na criação da versão, não na execução. Isso significa que um XML quebrado falha para quem está subindo a regra e recebe 400 na hora — nunca para o cliente final no meio de uma proposta.

O formato é aberto. DMN é um padrão da OMG, na versão 1.5 desde agosto de 2024. A tabela que você escreve aqui é um XML padronizado — ele sai daqui inteiro e roda em outro motor conforme. Isso não é detalhe técnico: é a diferença entre depender de um fornecedor e depender de um formato.


04

Casos de uso reais

negócio

Caso 1 — Uma financeira aperta o critério de risco na sexta e a regra vale na sexta Cenário ilustrativo

Contexto

Financeira de crédito pessoal com originação digital, cerca de 40 mil propostas por mês. A política de aprovação tem 6 entradas (score de bureau, renda declarada, renda estimada, comprometimento, produto, canal) e 30 linhas.

A dor

A política morava numa classe de serviço com condicionais aninhadas. Toda mudança de faixa passava por chamado, desenvolvimento, revisão e janela de deploy — na prática, de dez a quinze dias úteis entre o comitê de risco decidir e a regra valer. Quando a inadimplência de uma safra subia, a financeira originava mais duas semanas com o critério que já sabia estar errado.

A solução com o BB

A política vira uma decisão politica-credito-pessoal dentro do projeto credito. Cada revisão do comitê vira uma versão em semver: POST /decision-engine/api/v1/decisions/:decisionId/versions cria a 2.3.0 em DRAFT, o time de risco a valida contra uma amostra pela rota POST /decision-engine/api/v1/versions/:versionId/execute — que executa uma versão específica sem afetar quem está em produção — e POST /decision-engine/api/v1/versions/:versionId/publish faz ela valer. O aplicativo continua chamando POST /decision-engine/api/v1/decisions/:decisionId/execute, sempre sem saber que versão está no ar.

O resultado

O intervalo entre decidir e valer cai de semanas para o tempo de escrever a tabela e revisar. E o ExecutionLog responde, para qualquer proposta, com qual versão ela foi avaliada.

flowchart LR
  C["Comitê de risco revisa a faixa"] --> V["POST /decisions/:decisionId/versions cria a 2.3.0 em DRAFT"]
  V --> T["POST /versions/:versionId/execute contra uma amostra"]
  T --> Q{"Amostra bate com o esperado?"}
  Q -->|"não"| C
  Q -->|"sim"| P["POST /versions/:versionId/publish"]
  P --> A["A 2.3.0 vira a versão publicada mais recente"]
  APP["Aplicativo de originação"] -->|"sempre a mesma chamada"| E["POST /decisions/:decisionId/execute"]
  E --> A
  A --> L["ExecutionLog grava versionUsed 2.3.0"]

Caso 2 — Uma auditoria pergunta por uma negativa de dezoito meses atrás Cenário ilustrativo

Contexto

Operação de crédito consignado que precisa responder a uma reclamação formal sobre negativa de proposta.

A dor

A política tinha mudado sete vezes desde então. Reconstruir a regra vigente naquela data exigia achar o commit certo, entender o que mais tinha mudado junto e confiar que o comportamento em produção era mesmo o do código. Nenhuma dessas três etapas dá uma resposta que se sustenta num processo.

A solução com o BB

GET /decision-engine/api/v1/execution-logs?filter[decisionId]=...&filter[from]=...&filter[to]=... devolve as execuções da janela. Cada log traz o versionId usado, a entrada exata e a saída. GET /decision-engine/api/v1/versions/:versionId devolve o XML DMN daquela versão — a tabela que de fato avaliou aquela proposta, não uma reconstrução dela.

O resultado

A resposta deixa de ser uma reconstrução e passa a ser um registro. E como a tabela DMN é legível, ela entra no processo como anexo em vez de como código-fonte.

sequenceDiagram
  autonumber
  actor J as Jurídico
  participant DE as Decision Engine
  participant DB as Banco decisions
  J->>DE: GET /execution-logs com filtro de data
  DE->>DB: consulta execution_logs da organização
  DB-->>DE: execuções da janela
  DE-->>J: lista com versionId, input e output
  J->>DE: GET /versions/{versionId}
  DE-->>J: XML DMN que de fato avaliou a proposta
  Note over J,DE: A resposta ao processo é o registro, não uma reconstrução

Caso 3 — Uma seguradora troca regra de aceitação por produto sem multiplicar código Cenário ilustrativo

Contexto

Seguradora com 12 produtos, cada um com sua tabela de aceitação e seus critérios de recusa automática.

A dor

Cada produto novo significava mais um ramo de condicional no mesmo serviço. O acoplamento crescia, e mudar a regra do produto A quebrava o produto B com uma frequência desconfortável.

A solução com o BB

Cada produto vira uma Decision própria dentro do projeto aceitacao, com sua chave (aceitacao-vida, aceitacao-auto). As DecisionTag marcam quais são as tabelas reguladas e quais são comerciais, e GET /decision-engine/api/v1/decisions?filter[tagId]=... lista por corte. O aplicativo resolve a chave do produto e chama a decisão correspondente.

O resultado

Regras de produtos diferentes deixam de compartilhar código. Produto novo é um POST, não uma refatoração.

flowchart LR
  APP["Aplicativo resolve a chave do produto"] --> K{"Qual produto?"}
  K -->|"vida"| D1["Decision aceitacao-vida"]
  K -->|"auto"| D2["Decision aceitacao-auto"]
  K -->|"demais 10 produtos"| D3["Decision aceitacao-..."]
  subgraph P["Projeto aceitacao"]
    D1
    D2
    D3
  end
  D1 --> T1["Tag regulada"]
  D2 --> T2["Tag comercial"]
  T1 --> F["GET /decisions com filtro por tagId"]
  T2 --> F

Caso 4 — Por que um padrão aberto importa na hora de trocar de fornecedor Referência de mercado

Contexto

O DMN é mantido pela OMG, o mesmo consórcio do BPMN e do UML, e está na versão 1.5 desde agosto de 2024. A especificação define uma notação gráfica, um modelo de tabela de decisão e a linguagem de expressão FEEL, com representação em XML pensada explicitamente para intercâmbio entre organizações e ferramentas.

A dor do mercado

Boa parte dos motores de regras de mercado usa formato proprietário. O GoRules, por exemplo, é tecnicamente muito bom e tem motor open source em nove linguagens, mas o modelo de decisão nativo dele é próprio. Sair de um formato proprietário significa reescrever a política inteira, e é exatamente por isso que a migração de BRMS raramente acontece mesmo quando o contrato incomoda.

Como a Catalisa endereça

O que o Decision Engine armazena em DecisionVersion.content é o XML DMN cru. Ele entra por API e sai por API pela rota GET /decision-engine/api/v1/versions/:versionId, que devolve o conteúdo completo. Uma decisão de sair da Catalisa custa o tempo de baixar os XMLs, não o de reescrever a política.

O resultado

O custo de troca é uma escolha nossa, não uma consequência de arquitetura. Isso é defensável em due diligence de um jeito que "temos API de exportação" não é.

flowchart LR
  A["Tabela escrita no Camunda Modeler"] --> B["XML DMN 1.5 da OMG"]
  B --> C["DecisionVersion.content na Catalisa"]
  C -->|"GET /versions/:versionId"| D["XML DMN cru de volta"]
  D --> E["Qualquer motor conforme ao padrão"]
  F["Formato proprietário de concorrente"] -.->|"saída exige reescrever a política"| G["Migração que raramente acontece"]

05

Mercado e diferenciais

negócio

Panorama. O mercado de motores de regras se divide em três faixas. Na de cima estão as suítes corporativas — IBM ODM, FICO Blaze Advisor — que entregam governança completa, simulação e ferramenta de autoria para o analista, com preço por cotação e ciclo de implantação de projeto. No meio estão os motores open source operáveis — Camunda e Drools, este último hoje sob a Apache Software Foundation com licença Apache 2.0 — que resolvem a execução e transferem a operação, o versionamento e o multi-tenant para você. Embaixo estão as bibliotecas — json-rules-engine, GoRules — que embarcam no seu processo e não têm opinião sobre governança.

flowchart TD
  R["Como o mercado avalia regras hoje"] --> A["Faixa de cima: suítes corporativas"]
  R --> B["Faixa do meio: motores open source operáveis"]
  R --> C["Faixa de baixo: bibliotecas embarcáveis"]
  A --> A1["IBM ODM e FICO Blaze Advisor"]
  A1 --> A2["Governança, simulação e autoria completas — preço por cotação e implantação de projeto"]
  B --> B1["Camunda e Drools sob a Apache 2.0"]
  B1 --> B2["Resolvem a execução; operação, versionamento e multi-tenant ficam com você"]
  C --> C1["json-rules-engine e GoRules"]
  C1 --> C2["Embarcam no seu processo e não têm opinião sobre governança"]

O Decision Engine não tenta ser o motor mais rápido nem o modelador mais bonito. Ele pega um motor DMN maduro e coloca em volta dele o que falta para uso em produção regulada: versão, publicação, log por execução, isolamento por organização e autorização — tudo com o mesmo token dos outros 31 building blocks.

CritérioCatalisa Decision EngineCamundaDrools (Apache KIE)IBM ODMjson-rules-engine
Formato da regraDMN (XML da OMG)DMN (XML da OMG)DRL e DMNProprietário e DMNJSON próprio
PreçoPrecificação em definiçãoCotação (tabela não publica valores)Licença zero, você operaCotação, não públicoGratuito (ISC)
Versionamento e publicaçãoNativo, com semver e estadosVia deploy de definiçãoVocê constróiNativoNão existe
Log por execuçãoNativo, com versão usadaVia histórico da plataformaVocê constróiNativoNão existe
Multi-tenantNativo, por organizationId do tokenVocê modelaVocê constróiVocê modelaNão existe
Autorização por permissãoNativa, vocabulário compartilhadoExternaExternaNativaNão existe
Modelador visual de tabelaNão (ver §15)Sim, referência de mercadoParcialSimNão
Simulação e teste em loteNão (ver §15)SimParcialSimNão
Você opera o motorNãoSim, salvo SaaSSimSimN/A

Nossos diferenciais

  1. A governança vem junto, e não em volta. Versão, publicação, arquivamento e log de execução são endpoints do produto. Quem adota Camunda ou Drools recebe um avaliador de regras excelente e ainda precisa construir essas quatro coisas — que são justamente as que a auditoria pede.
  2. O tenant é do token, não do modelo de dados. Toda rota exige organizationId no JWT e todo repositório filtra por ele. Num motor genérico, separar clientes é modelagem sua, e modelagem sua é onde vaza.
  3. O formato é padrão, então a saída é barata. A regra é XML DMN da OMG, extraível por API. Isso é difícil de copiar não por ser tecnicamente complexo, mas porque o modelo de negócio da maioria dos concorrentes depende do custo de troca.
  4. O motor é substituível por trás. O DmnProxyService fala com um runner DMN por HTTP através de uma interface de três operações — executar, validar e checar saúde. Trocar o runner não toca nenhuma rota, nenhum dado e nenhum cliente.

Quando escolher o concorrente.

flowchart TD
  Q1{"Seu analista precisa desenhar a tabela numa interface gráfica?"}
  Q1 -->|"sim, e o time não aceita modelar fora"| CAM["Camunda, a plataforma inteira"]
  Q1 -->|"não, ou aceita modelar no Modeler e subir o XML"| Q2
  Q2{"Precisa de simulação em lote, A/B de políticas e teste governado com aprovação formal?"}
  Q2 -->|"sim"| IBM["IBM ODM ou FICO Blaze Advisor"]
  Q2 -->|"não"| Q3
  Q3{"A regra é simples, cabe num serviço Node e dispensa versionamento e auditoria?"}
  Q3 -->|"sim"| JRE["json-rules-engine"]
  Q3 -->|"não"| Q4
  Q4{"Você já opera JVM e tem time para isso?"}
  Q4 -->|"sim"| DRO["Drools sob a Apache 2.0"]
  Q4 -->|"não"| CAT["Catalisa Decision Engine"]

Se o seu analista de negócio precisa desenhar a tabela numa interface gráfica, o Camunda Modeler é hoje a melhor ferramenta de autoria DMN do mercado e o Decision Engine não tem editor visual (§15) — o caminho honesto é modelar no Camunda Modeler e subir o XML aqui, mas se o seu time não aceita esse passo, use a plataforma deles inteira.

Se você precisa de simulação em lote, comparação A/B entre políticas e um ambiente de teste governado com aprovação formal, IBM ODM e FICO Blaze Advisor entregam isso há duas décadas e nós não entregamos.

Se a sua regra é simples, roda dentro de um único serviço Node e não precisa de versionamento nem auditoria, o json-rules-engine é gratuito, tem 17 kB e resolve em uma tarde — adotar um building block inteiro seria excesso. E se você já opera JVM e tem time para isso, o Drools sob a Apache 2.0 não custa licença nenhuma.


06

Modelo de cobrança e ROI

negócio

Unidade de cobrança. Precificação em definição. Quando definida, a unidade natural é a execução de decisão — é o que o cliente entende, é o que escala com o uso e é o que aparece no ExecutionLog.

O que dispara custo.

DriverPor que ele importa
Execuções por mêsÉ a chamada ao runner DMN, o custo variável real
Decisões publicadasProxy do tamanho da operação de política
Retenção do log de execuçãoCada execução grava uma linha com entrada e saída em JSON; auditoria longa custa armazenamento

Comparação de custo. Cenário: operação com 500 mil execuções de decisão por mês e 20 decisões publicadas.

Catalisa Decision EngineCamunda Self-ManagedDrools (Apache KIE)IBM ODM
LicençaPrecificação em definiçãoCotação — a tabela pública não publica valoresZero (Apache 2.0)Cotação, não público
Quem opera o motorCatalisaVocêVocêVocê ou parceiro
Versionamento e logIncluídoVocê constróiVocê constróiIncluído
Multi-tenantIncluídoVocê modelaVocê constróiVocê modela

Comparação estruturada em 2026-08-16 a partir das páginas públicas dos fornecedores. Nenhum dos concorrentes comerciais publica preço — Camunda e IBM vendem por cotação. Não estimamos valores que não conseguimos verificar. Consulte cada fornecedor na data da sua análise.

ROI. A conta que fecha não é a de licença — o Drools é gratuito e vai ganhar de qualquer coisa nessa linha. O retorno está em dois lugares.

Onde está o retornoO que ele evita
O que você não constróiVersionamento, publicação, log por execução e multi-tenant sobre um motor cru
O intervalo entre decidir e valerOriginação com a regra que o comitê já decidiu abandonar

O primeiro é o que você não constrói: versionamento, publicação, log por execução e multi-tenant sobre um motor cru são um projeto de meses, e é um projeto que precisa ficar certo porque ele é o que a auditoria olha.

O segundo é o intervalo entre decidir e valer: numa carteira que origina 40 mil propostas por mês, cada semana de atraso em apertar um critério de risco é originação inteira com a regra que o comitê já decidiu abandonar. Esse número é seu e você sabe calculá-lo melhor que nós.


07

Arquitetura

As camadas

O building block é um app Hono com basePath('/decision-engine'), uma camada de serviços em neverthrow e duas saídas: o PostgreSQL, por Prisma, e o runner DMN, por HTTP.

flowchart TD
  CLI["Cliente HTTP com Bearer JWT do IAM"] --> HONO

  subgraph HONO["Hono app — basePath /decision-engine"]
    MW["Toda rota: authMiddleware, requirePermission(P), requireOrganization"]
    R1["projectsRouter"]
    R2["decisionsRouter"]
    R3["versionsRouter"]
    R4["tagsRouter"]
    R5["executionLogsRouter"]
    R6["/health — sonda simples"]
  end

  HONO -->|"Zod parse, depois ResultAsync de T ou AppError"| SVC

  subgraph SVC["services/"]
    S1["ProjectService"]
    S2["DecisionService"]
    S3["VersionService"]
    S4["TagService"]
    S5["ExecutionLogService"]
    S6["DmnProxyService"]
  end

  SVC --> REPO["repositories/ com Prisma — PostgreSQL, schema decisions"]
  S6 -->|"HTTP"| RUNNER

  subgraph RUNNER["runner DMN em DMN_ENGINE_URL"]
    RU1["POST /api/v1/execute"]
    RU2["GET /health"]
    RU3["imagem decision-engine-runner — Camunda DMN, repositório à parte"]
  end

Os cinco routers e o que cada um monta:

Prefixo montadoRouterRotas
/api/v1/projectsprojectsRouter5
/api/v1/decisionsdecisionsRouter6
/api/v1versionsRouter6
/api/v1/tagstagsRouter5
/api/v1/execution-logsexecutionLogsRouter3
/health—sonda simples

E o que cada serviço faz:

ServiçoResponsabilidade
ProjectServiceAgrupamento de decisões
DecisionServiceCiclo de vida + execute (usa a última PUBLISHED)
VersionServiceSemver, publish, archive, execute de versão fixa
TagServiceRótulos por organização
ExecutionLogServiceConsulta do rastro
DmnProxyServiceÚnica porta de saída para o runner DMN

O caminho de uma execução

Uma chamada a POST /decision-engine/api/v1/decisions/:decisionId/execute com { "input": { "age": 25 } } percorre seis etapas, e três delas podem interrompê-la:

sequenceDiagram
  autonumber
  actor C as Cliente
  participant H as Hono + middlewares
  participant DS as DecisionService
  participant DB as PostgreSQL
  participant PX as DmnProxyService
  participant RU as runner DMN
  participant EV as Barramento de eventos

  C->>H: POST /decisions/{id}/execute com input
  H->>DS: executeDecision após Zod parse
  DS->>DB: busca a Decision na organização do token
  alt Decision não existe
    DS-->>C: 404
  else Decision não está ACTIVE
    DS-->>C: 400 Decision is not active
  end
  DS->>DB: busca a DecisionVersion PUBLISHED mais recente
  alt Nenhuma versão publicada
    DS-->>C: 400 No published version available
  end
  DS->>PX: execute com o content da versão e o input
  PX->>RU: POST /api/v1/execute com dmnContent e data
  RU-->>PX: result, decisions e sucesso
  PX-->>DS: output, trace e executionTimeMs
  DS->>DB: grava ExecutionLog
  DS->>EV: publica decision-engine.decision.executed
  DS-->>C: 200 com output, trace, executionTimeMs e versionUsed

As mesmas seis etapas, com o que reprova cada uma:

#EtapaSe falhar
1Decision existe nesta organização?404
2Decision.status == ACTIVE?400
3Existe DecisionVersion PUBLISHED? Pega a mais recente publicada400
4DmnProxyService.execute(version.content, input) → POST $DMN_ENGINE_URL/api/v1/execute400 com a mensagem do runner
5Grava ExecutionLog (entrada, saída, trace, tempo, quem)—
6Publica o evento decision-engine.decision.executed500 Failed to publish event

A resposta de sucesso é 200 { output, trace, executionTimeMs, versionUsed }.

Decisão: o motor DMN é um processo separado, alcançado por HTTP

O DmnProxyService é a única porta de saída, com três operações: execute, validateDmn e healthCheck. O runner é a imagem decision-engine-runner, construída a partir de um repositório próprio (catalisaio/decision-engine-runner) e publicada no GHCR pelo workflow build-dmn-engine.yml. O motivo é isolamento de tecnologia: a avaliação DMN madura vive na JVM, o resto da plataforma é Bun e TypeScript, e essa fronteira mantém as duas coisas independentes. O custo aceito é uma chamada de rede por execução.

Decisão: o runner não conhece organização, e por isso não recebe nada persistido

Cada execução envia o XML DMN inteiro no corpo da requisição, junto com a entrada. O runner é stateless por construção — não há deploy de modelo, não há catálogo dentro dele, não há estado para vazar entre clientes. O custo é trafegar o XML a cada chamada; o ganho é que o isolamento entre tenants nunca depende do runner.

flowchart LR
  A["Catalisa guarda o XML em DecisionVersion.content"] -->|"envia XML + input a cada chamada"| B["runner DMN"]
  B -->|"result + trace"| A
  B --- C["Sem modelo implantado"]
  B --- D["Sem catálogo interno"]
  B --- E["Sem estado entre chamadas"]

Decisão: a validação do DMN acontece na criação da versão, não na execução

VersionService.createVersion chama validateDmn antes de gravar. Um XML quebrado falha para quem está subindo a regra, e não para o cliente final na primeira proposta. O trade-off é uma ida ao runner a cada versão criada, o que é irrelevante na frequência em que isso acontece.

Decisão: dois endpoints de execução, com propósitos diferentes

Executar uma decisão usa sempre a última versão publicada; executar uma versão usa aquela versão.

RotaPropósitoVersão usadaDevolve trace?
POST /decisions/:id/executeProdução — o chamador não sabe nem quer saber a versãoA PUBLISHED mais recenteSim
POST /versions/:id/executeTeste — você escolhe a versão de propósitoA que você indicarNão

POST /versions/:id/execute é o caminho de teste: você escolhe a versão de propósito, inclusive uma que acabou de publicar, para comparar contra a anterior.

Decisão: só decisão ACTIVE executa

Uma Decision nasce DRAFT e precisa ser promovida a ACTIVE por PATCH. É um segundo interruptor, além da publicação da versão: escrever a tabela, publicar a versão e ligar a decisão são três atos deliberados. Isso irrita na primeira integração e evita o acidente na centésima.

Decisão: exclusão é lógica

Projetos, decisões, versões e tags usam deletedAt. O log de execução aponta para a versão, e a versão precisa sobreviver à remoção para que a auditoria continue reproduzível.

Monolito vs. standalone

Em monolito, o container TypeDI resolve os serviços por chamada direta. Em standalone — o modo usado em produção — o serviço sobe na porta 3005 com MODULE_SELF=decision-engine. Em qualquer um dos modos o runner DMN é sempre alcançado por HTTP: o DMN_ENGINE_URL é obrigatório nos dois.

Atenção. Outros building blocks que só precisam avaliar um DMN sem persistir nada usam a facade DecisionEngineFacadeToken, que tem implementação local e remota.


08

Conceitos e modelo de dados

Glossário

TermoSignifica
DMNDecision Model and Notation, padrão da OMG para modelar decisões. A regra é um XML.
FEELFriendly Enough Expression Language, a linguagem de expressão do DMN. É o que você escreve numa célula: >= 18, "adult".
Tabela de decisãoO <decisionTable> do DMN: colunas de entrada, colunas de saída, uma linha por regra.
Hit policyComo o motor resolve quando mais de uma linha casa. FIRST devolve a primeira; há outras no padrão.
ProjectAgrupamento de decisões. Existe para organizar, não para isolar — o isolamento é por organização.
DecisionA decisão de negócio, com chave estável (politica-credito-pessoal). É o que o seu aplicativo referencia.
DecisionVersionUma versão do XML DMN daquela decisão, em semver. É o que de fato executa.
TagRótulo colorido por organização, para cortar o catálogo de decisões.
ExecutionLogO registro de uma execução: entrada, saída, tempo, quem executou e qual versão.

Modelo de dados — schema decisions no PostgreSQL.

Modelo PrismaTabelaPropósitoCampos-chave
DecisionProjectdecisions.decision_projectsAgrupa decisõeskey único por organização, ownerId, deletedAt
Decisiondecisions.decisionsA decisão de negóciokey único por organização, projectId, status, deletedAt
DecisionVersiondecisions.decision_versionsO XML DMN versionadocontent (texto), version único por decisão, status, publishedAt
DecisionTagdecisions.decision_tagsRótulo por organizaçãoname único por organização, color (hex)
DecisionTagLinkdecisions.decision_tag_linksLiga decisão e tagChave composta (decisionId, tagId)
ExecutionLogdecisions.execution_logsRastro de execuçãodecisionId, versionId, input, output, executionTimeMs, executedBy

Enumerações

EnumValores
DecisionStatusDRAFT · ACTIVE · ARCHIVED
VersionStatusDRAFT · PUBLISHED · ARCHIVED

Relacionamentos

erDiagram
  DecisionProject ||--o{ Decision : agrupa
  Decision ||--o{ DecisionVersion : versiona
  Decision ||--o{ DecisionTagLink : rotula
  DecisionTag ||--o{ DecisionTagLink : usada_em
  Decision ||--o{ ExecutionLog : registra
  DecisionVersion ||--o{ ExecutionLog : avaliou

  DecisionProject {
    uuid id PK
    uuid organization_id
    string key "único por organização"
    uuid owner_id
    datetime deleted_at
  }
  Decision {
    uuid id PK
    uuid organization_id
    uuid project_id FK
    string key "único por organização"
    enum status "DRAFT ACTIVE ARCHIVED"
    datetime deleted_at
  }
  DecisionVersion {
    uuid id PK
    uuid decision_id FK
    string version "semver, único por decisão"
    text content "o XML DMN"
    enum status "DRAFT PUBLISHED ARCHIVED"
    datetime published_at
  }
  DecisionTag {
    uuid id PK
    uuid organization_id
    string name "único por organização"
    string color "hex RRGGBB"
  }
  DecisionTagLink {
    uuid decision_id PK
    uuid tag_id PK
  }
  ExecutionLog {
    uuid id PK
    uuid decision_id FK
    uuid version_id FK
    json input
    json output
    int execution_time_ms
    uuid executed_by
  }

Repare que DecisionVersion e ExecutionLog não têm organization_id — o isolamento delas é por junção com a decisão dona. É um detalhe que reaparece em §14.

Ciclo de vida de uma decisão

A Decision é o interruptor de negócio: ela nasce em rascunho e alguém precisa ligá-la.

stateDiagram-v2
  [*] --> DRAFT: POST /decisions
  DRAFT --> ACTIVE: PATCH status ACTIVE
  ACTIVE --> ARCHIVED: PATCH status ARCHIVED
  ACTIVE --> ACTIVE: executa
  note right of ACTIVE
    Só decisão ACTIVE responde por
    execute da decisão
  end note

Atenção. O PATCH grava o status que você mandar, sem validar a transição. Na prática dá para voltar de ARCHIVED para ACTIVE — o desenho acima é o caminho pretendido, não uma trava do serviço.

Ciclo de vida de uma versão

A DecisionVersion é o interruptor de conteúdo: ela guarda o XML e só vale depois de publicada.

stateDiagram-v2
  [*] --> DRAFT: POST versions
  DRAFT --> PUBLISHED: POST publish
  DRAFT --> ARCHIVED: POST archive
  PUBLISHED --> ARCHIVED: POST archive
  ARCHIVED --> [*]
  note left of DRAFT
    Não executa
  end note
  note right of PUBLISHED
    A mais recente publicada é a que
    responde por execute da decisão
  end note
  note right of ARCHIVED
    Não executa mais, e publicar de
    novo devolve 400
  end note
TransiçãoEndpoint
criação → DRAFTPOST /decision-engine/api/v1/decisions/:decisionId/versions
DRAFT → PUBLISHEDPOST /decision-engine/api/v1/versions/:versionId/publish
DRAFT ou PUBLISHED → ARCHIVEDPOST /decision-engine/api/v1/versions/:versionId/archive

Juntando os dois interruptores:

flowchart LR
  A{"Decision.status == ACTIVE?"}
  A -->|"não"| E400A["400"]
  A -->|"sim"| B{"Existe DecisionVersion PUBLISHED?"}
  B -->|"não"| E400B["400"]
  B -->|"sim"| OK["Executa com a versão publicada mais recente"]

Atenção. Para uma proposta ser avaliada, ela precisa de Decision.status == ACTIVE e de pelo menos uma DecisionVersion PUBLISHED. Faltando um dos dois, a resposta é 400, não 404. É a causa nº 1 de dúvida na integração.

Publicar não despublica a anterior: várias versões podem estar PUBLISHED ao mesmo tempo, e o motor usa a mais recente. Para tirar uma do ar, arquive-a.


09

Referência da API

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

Todas as rotas abaixo exigem: authMiddleware (Bearer JWT do IAM), o requirePermission indicado na coluna, e requireOrganization — token sem organizationId recebe 403 antes de a regra de negócio rodar.

Os corpos aceitam duas formas: JSON:API ({"data":{"attributes":{...}}}) ou o objeto direto ({...}). O código faz body.data?.attributes ?? body.

Os 25 endpoints se organizam em cinco grupos, e a única fronteira que confunde é a das duas rotas de execução:

flowchart LR
  API["/decision-engine/api/v1"] --> P["projects — 5 rotas"]
  API --> D["decisions — 6 rotas"]
  API --> V["versions — 6 rotas"]
  API --> T["tags — 5 rotas"]
  API --> L["execution-logs — 3 rotas"]
  D --> DE["POST /decisions/:decisionId/execute — produção"]
  V --> VE["POST /versions/:versionId/execute — teste"]

Projetos — /decision-engine/api/v1/projects

MétodoRotaDescriçãoPermissão
POST/decision-engine/api/v1/projectsCria projetoDECISION_PROJECTS_CREATE
GET/decision-engine/api/v1/projectsLista projetos, paginadoDECISION_PROJECTS_READ
GET/decision-engine/api/v1/projects/:projectIdBusca projetoDECISION_PROJECTS_READ
PATCH/decision-engine/api/v1/projects/:projectIdAtualiza nome e descriçãoDECISION_PROJECTS_UPDATE
DELETE/decision-engine/api/v1/projects/:projectIdExclusão lógica. Responde 204DECISION_PROJECTS_DELETE

Decisões — /decision-engine/api/v1/decisions

MétodoRotaDescriçãoPermissão
POST/decision-engine/api/v1/decisionsCria decisão em DRAFTDECISION_VERSIONS_CREATE
GET/decision-engine/api/v1/decisionsLista decisões, paginadoDECISION_VERSIONS_READ
GET/decision-engine/api/v1/decisions/:decisionIdBusca decisãoDECISION_VERSIONS_READ
PATCH/decision-engine/api/v1/decisions/:decisionIdAtualiza nome, descrição, status e tagsDECISION_VERSIONS_CREATE
DELETE/decision-engine/api/v1/decisions/:decisionIdExclusão lógica. Responde 204DECISION_PROJECTS_DELETE
POST/decision-engine/api/v1/decisions/:decisionId/executeExecuta a última versão publicadaDECISION_EXECUTE

As permissões desta tabela estão transcritas do código, não normalizadas. Repare que criar e atualizar decisão exigem DECISION_VERSIONS_CREATE e que excluir exige DECISION_PROJECTS_DELETE — não há permissão dedicada a decisões. Conceda os papéis com base nesta tabela, não no nome que a permissão sugere.

Filtros de GET /decisions: filter[projectId] (UUID), filter[status] (DRAFT, ACTIVE, ARCHIVED), filter[tagId] (UUID).

Versões — /decision-engine/api/v1

MétodoRotaDescriçãoPermissão
POST/decision-engine/api/v1/decisions/:decisionId/versionsCria versão. Valida o DMN antes de gravarDECISION_VERSIONS_CREATE
GET/decision-engine/api/v1/decisions/:decisionId/versionsLista versões da decisãoDECISION_VERSIONS_READ
GET/decision-engine/api/v1/versions/:versionIdBusca versão. Devolve o XML DMN em contentDECISION_VERSIONS_READ
POST/decision-engine/api/v1/versions/:versionId/publishPublica a versãoDECISION_VERSIONS_PUBLISH
POST/decision-engine/api/v1/versions/:versionId/archiveArquiva a versãoDECISION_VERSIONS_CREATE
POST/decision-engine/api/v1/versions/:versionId/executeExecuta esta versão específicaDECISION_EXECUTE

Filtro de listagem: filter[status] (DRAFT, PUBLISHED, ARCHIVED).

Tags — /decision-engine/api/v1/tags

MétodoRotaDescriçãoPermissão
POST/decision-engine/api/v1/tagsCria tagDECISION_TAGS_MANAGE
GET/decision-engine/api/v1/tagsLista tags, paginadoDECISION_TAGS_MANAGE
GET/decision-engine/api/v1/tags/:tagIdBusca tagDECISION_TAGS_MANAGE
PATCH/decision-engine/api/v1/tags/:tagIdAtualiza nome e corDECISION_TAGS_MANAGE
DELETE/decision-engine/api/v1/tags/:tagIdRemove tag. Responde 204DECISION_TAGS_MANAGE

Não há permissão de leitura separada para tags: ler exige a mesma DECISION_TAGS_MANAGE que escrever.

Logs de execução

MétodoRotaDescriçãoPermissão
GET/decision-engine/api/v1/execution-logsLista todos os logs da organizaçãoDECISION_LOGS_READ
GET/decision-engine/api/v1/execution-logs/:logIdBusca um logDECISION_LOGS_READ
GET/decision-engine/api/v1/decisions/:decisionId/execution-logsLogs de uma decisãoDECISION_LOGS_READ

Filtros: filter[decisionId], filter[versionId] (UUID), filter[from] e filter[to] (data ISO-8601).

O executionLogsRouter está montado em dois pontos (/api/v1/execution-logs e /api/v1), o que faz caminhos equivalentes responderem sob outros prefixos. Use apenas as três rotas da tabela — são as que a documentação sustenta.

Saúde

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

POST /decision-engine/api/v1/decisions/:decisionId/versions

Cria uma nova versão da decisão. O XML DMN é validado contra o runner antes de ser gravado.

Request

json
{
  "version": "1.0.0",
  "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><definitions ...>...</definitions>"
}
{
  "version": "1.0.0",
  "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><definitions ...>...</definitions>"
}
CampoTipoObrigatórioDescrição
versionstringSimSemver estrito, no formato \d+\.\d+\.\d+. Único por decisão.
contentstringSimO XML DMN completo. Mínimo 1 caractere; validado no runner.

Resposta 201

json
{
  "data": {
    "type": "decision-versions",
    "id": "8f1c...",
    "links": { "self": "/api/v1/versions/8f1c..." },
    "attributes": {
      "decisionId": "3a2b...",
      "version": "1.0.0",
      "status": "DRAFT",
      "publishedAt": null
    }
  }
}
{
  "data": {
    "type": "decision-versions",
    "id": "8f1c...",
    "links": { "self": "/api/v1/versions/8f1c..." },
    "attributes": {
      "decisionId": "3a2b...",
      "version": "1.0.0",
      "status": "DRAFT",
      "publishedAt": null
    }
  }
}

Erros

StatusQuando
400version fora de semver, content vazio, ou DMN inválido segundo o runner
403Token sem organizationId ou sem DECISION_VERSIONS_CREATE
404A decisão não existe nesta organização
409Já existe uma versão com esse número nesta decisão

POST /decision-engine/api/v1/decisions/:decisionId/execute

O endpoint de produção. Usa a versão publicada mais recente, sem que o chamador precise saber qual é.

Request

json
{ "input": { "age": 25, "score": 730, "renda": 5200 } }
{ "input": { "age": 25, "score": 730, "renda": 5200 } }
CampoTipoObrigatórioDescrição
inputobjectSimMapa livre de variáveis. As chaves precisam casar com os inputExpression da tabela DMN.

Resposta 200

json
{
  "output": { "result": "adult" },
  "trace": [
    {
      "decisionId": "age_check",
      "decisionName": "Age Check",
      "outcome": { "result": "adult" },
      "rulesEvaluated": [
        { "ruleId": "rule1", "triggered": true, "conditions": [">= 18"], "outcome": {} }
      ]
    }
  ],
  "executionTimeMs": 34,
  "versionUsed": "1.0.0"
}
{
  "output": { "result": "adult" },
  "trace": [
    {
      "decisionId": "age_check",
      "decisionName": "Age Check",
      "outcome": { "result": "adult" },
      "rulesEvaluated": [
        { "ruleId": "rule1", "triggered": true, "conditions": [">= 18"], "outcome": {} }
      ]
    }
  ],
  "executionTimeMs": 34,
  "versionUsed": "1.0.0"
}

O trace é a explicação da decisão: quais regras foram avaliadas e qual disparou. É o que responde "por que este cliente foi negado" — guarde-o.

Erros

StatusQuando
400Decision is not active — a decisão não está ACTIVE
400No published version available — nenhuma versão publicada
400DMN execution failed: ... — o runner recusou o XML ou a entrada
403Token sem organizationId ou sem DECISION_EXECUTE
404A decisão não existe nesta organização

Execução que falha não gera ExecutionLog. O registro só é gravado depois que o runner responde com sucesso. Para monitorar taxa de erro, use o log da aplicação e não a tabela — ver §15.


POST /decision-engine/api/v1/versions/:versionId/execute

O endpoint de teste. Executa a versão que você indicar, desde que ela esteja PUBLISHED. Mesmo corpo do anterior.

Resposta 200

json
{ "output": { "result": "adult" }, "executionTimeMs": 31 }
{ "output": { "result": "adult" }, "executionTimeMs": 31 }

Repare que esta rota não devolve trace nem versionUsed — você já sabe a versão, e a explicação não é retornada aqui. Se precisa do trace, use a rota da decisão.

Erros

StatusQuando
400Only published versions can be executed — a versão está DRAFT ou ARCHIVED
403A versão pertence a outra organização, ou falta DECISION_EXECUTE
404A versão não existe

10

Início rápido

Do zero à primeira decisão avaliada. Os comandos usam staging e a tabela DMN de exemplo é a mesma de scripts/test-dmn-staging.ts.

São sete passos, e o resultado do último é uma decisão avaliada com rastro gravado:

flowchart LR
  P1["1. Autenticar no IAM"] --> P2["2. Criar o projeto"]
  P2 --> P3["3. Criar a decisão"]
  P3 --> P4["4. Criar a versão com a tabela DMN"]
  P4 --> P5["5. Publicar a versão e ativar a decisão"]
  P5 --> P6["6. Executar"]
  P6 --> P7["7. Conferir o rastro"]

1. Autenticar no IAM

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

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

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

Confira que o token chegou antes de seguir — sem ele, todos os passos seguintes respondem 401:

bash
echo "${TOKEN:0:12}..."
# eyJhbGciOiJI...
echo "${TOKEN:0:12}..."
# eyJhbGciOiJI...

2. Criar o projeto

bash
PROJECT_ID=$(curl -s -X POST "$BASE/projects" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"name":"Crédito","key":"credito","description":"Políticas de crédito"}' \
  | jq -r '.data.id')
PROJECT_ID=$(curl -s -X POST "$BASE/projects" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"name":"Crédito","key":"credito","description":"Políticas de crédito"}' \
  | jq -r '.data.id')

Resposta esperada: o PROJECT_ID preenchido com o UUID do projeto recém-criado.

bash
echo "$PROJECT_ID"
# 7c9e6679-7425-40de-944b-e07fc1f90ae7
echo "$PROJECT_ID"
# 7c9e6679-7425-40de-944b-e07fc1f90ae7

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

3. Criar a decisão

bash
DECISION_ID=$(curl -s -X POST "$BASE/decisions" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d "{\"projectId\":\"$PROJECT_ID\",\"name\":\"Checagem de idade\",\"key\":\"checagem-idade\"}" \
  | jq -r '.data.id')
DECISION_ID=$(curl -s -X POST "$BASE/decisions" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d "{\"projectId\":\"$PROJECT_ID\",\"name\":\"Checagem de idade\",\"key\":\"checagem-idade\"}" \
  | jq -r '.data.id')

Resposta esperada: o DECISION_ID preenchido, e a decisão em rascunho.

bash
curl -s "$BASE/decisions/$DECISION_ID" -H "Authorization: Bearer $TOKEN" \
  | jq -r '.data.attributes.status'
# "DRAFT"
curl -s "$BASE/decisions/$DECISION_ID" -H "Authorization: Bearer $TOKEN" \
  | jq -r '.data.attributes.status'
# "DRAFT"

Ela nasce DRAFT. Ainda não executa.

4. Criar a versão com a tabela DMN

Primeiro, escreva a tabela num arquivo. Esta é uma tabela mínima com duas regras e a hit policy FIRST:

bash
cat > /tmp/age-check.dmn <<'DMN'
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="https://www.omg.org/spec/DMN/20191111/MODEL/"
             id="definitions" name="Age Check"
             namespace="http://camunda.org/schema/1.0/dmn">
  <decision id="age_check" name="Age Check">
    <decisionTable id="decisionTable" hitPolicy="FIRST">
      <input id="input1" label="Age">
        <inputExpression id="inputExpression1" typeRef="integer"><text>age</text></inputExpression>
      </input>
      <output id="output1" label="Result" name="result" typeRef="string" />
      <rule id="rule1">
        <inputEntry id="ie1"><text>&gt;= 18</text></inputEntry>
        <outputEntry id="oe1"><text>"adult"</text></outputEntry>
      </rule>
      <rule id="rule2">
        <inputEntry id="ie2"><text>&lt; 18</text></inputEntry>
        <outputEntry id="oe2"><text>"minor"</text></outputEntry>
      </rule>
    </decisionTable>
  </decision>
</definitions>
DMN
cat > /tmp/age-check.dmn <<'DMN'
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="https://www.omg.org/spec/DMN/20191111/MODEL/"
             id="definitions" name="Age Check"
             namespace="http://camunda.org/schema/1.0/dmn">
  <decision id="age_check" name="Age Check">
    <decisionTable id="decisionTable" hitPolicy="FIRST">
      <input id="input1" label="Age">
        <inputExpression id="inputExpression1" typeRef="integer"><text>age</text></inputExpression>
      </input>
      <output id="output1" label="Result" name="result" typeRef="string" />
      <rule id="rule1">
        <inputEntry id="ie1"><text>&gt;= 18</text></inputEntry>
        <outputEntry id="oe1"><text>"adult"</text></outputEntry>
      </rule>
      <rule id="rule2">
        <inputEntry id="ie2"><text>&lt; 18</text></inputEntry>
        <outputEntry id="oe2"><text>"minor"</text></outputEntry>
      </rule>
    </decisionTable>
  </decision>
</definitions>
DMN

Agora suba o arquivo como versão 1.0.0. O jq -Rs embrulha o XML inteiro como string JSON:

bash
VERSION_ID=$(jq -Rs '{version:"1.0.0", content:.}' < /tmp/age-check.dmn \
  | curl -s -X POST "$BASE/decisions/$DECISION_ID/versions" \
      -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d @- \
  | jq -r '.data.id')

curl -s "$BASE/versions/$VERSION_ID" -H "Authorization: Bearer $TOKEN" \
  | jq -r '.data.attributes | "\(.version) \(.status)"'
# 1.0.0 DRAFT
VERSION_ID=$(jq -Rs '{version:"1.0.0", content:.}' < /tmp/age-check.dmn \
  | curl -s -X POST "$BASE/decisions/$DECISION_ID/versions" \
      -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d @- \
  | jq -r '.data.id')

curl -s "$BASE/versions/$VERSION_ID" -H "Authorization: Bearer $TOKEN" \
  | jq -r '.data.attributes | "\(.version) \(.status)"'
# 1.0.0 DRAFT

Se o XML estiver quebrado, você recebe 400 aqui — antes de qualquer proposta real chegar.

5. Publicar a versão e ativar a decisão

Ligue o primeiro interruptor, o da versão:

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

Depois o segundo, o da decisão:

bash
curl -s -X PATCH "$BASE/decisions/$DECISION_ID" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"status":"ACTIVE"}' | jq '.data.attributes.status'
# "ACTIVE"
curl -s -X PATCH "$BASE/decisions/$DECISION_ID" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"status":"ACTIVE"}' | jq '.data.attributes.status'
# "ACTIVE"

São dois interruptores separados, e os dois precisam estar ligados.

6. Executar

bash
curl -s -X POST "$BASE/decisions/$DECISION_ID/execute" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"input":{"age":25}}' | jq
curl -s -X POST "$BASE/decisions/$DECISION_ID/execute" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"input":{"age":25}}' | jq
json
{
  "output": { "result": "adult" },
  "trace": [ { "decisionId": "age_check", "decisionName": "Age Check", "...": "..." } ],
  "executionTimeMs": 34,
  "versionUsed": "1.0.0"
}
{
  "output": { "result": "adult" },
  "trace": [ { "decisionId": "age_check", "decisionName": "Age Check", "...": "..." } ],
  "executionTimeMs": 34,
  "versionUsed": "1.0.0"
}

7. Conferir o rastro

bash
curl -s "$BASE/decisions/$DECISION_ID/execution-logs" \
  -H "Authorization: Bearer $TOKEN" | jq '.data[0].attributes | {versionId, input, executionTimeMs}'
curl -s "$BASE/decisions/$DECISION_ID/execution-logs" \
  -H "Authorization: Bearer $TOKEN" | jq '.data[0].attributes | {versionId, input, executionTimeMs}'

Resposta esperada — a execução do passo 6, com a versão que a avaliou:

json
{
  "versionId": "8f1c...",
  "input": { "age": 25 },
  "executionTimeMs": 34
}
{
  "versionId": "8f1c...",
  "input": { "age": 25 },
  "executionTimeMs": 34
}

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


11

Receitas

Trocar a política sem derrubar quem está integrado

O objetivo é substituir a regra vigente com verificação antes de ela valer.

flowchart LR
  A["1. Nova versão em DRAFT"] --> B["2. Publicar"]
  B --> C["3. Conferir com a versão fixada"]
  C -->|"passou"| D["Fim — a nova responde em produção"]
  C -->|"não passou"| E["4. Arquivar a nova"]
  E --> F["A anterior volta a ser a mais recente publicada"]

Passo 1. Crie a nova versão a partir do DMN revisado. Ela nasce DRAFT e não afeta produção:

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

Resposta esperada: um UUID em $VERSION_ID. Se vier vazio, o DMN foi recusado na validação.

Passo 2. Publique. A partir daqui ela é a mais recente publicada:

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

Passo 3. Confira contra um caso conhecido, agora com a versão fixada:

bash
curl -s -X POST "$BASE/versions/$VERSION_ID/execute" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"input":{"age":25}}' | jq
curl -s -X POST "$BASE/versions/$VERSION_ID/execute" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"input":{"age":25}}' | jq
json
{ "output": { "result": "adult" }, "executionTimeMs": 31 }
{ "output": { "result": "adult" }, "executionTimeMs": 31 }

Passo 4. Deu errado? Arquive a nova. A anterior volta a ser a mais recente publicada:

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

Armadilhas.

  • Publicar já muda produção. Não existe "publicar em homologação". A partir do passo 2 a versão nova está no ar, e a validação do passo 3 é depois. Para validar antes, mantenha uma decisão separada de ensaio ou valide numa organização de staging.
  • Publicar não despublica a anterior. Várias versões podem estar PUBLISHED ao mesmo tempo. O motor usa a mais recente. O jeito de voltar atrás é arquivar a nova, não republicar a antiga — republicar retorna 409, porque ela já está publicada.
  • Semver é obrigatório e imutável. 1.1 é rejeitado; precisa ser 1.1.0. E não há edição de versão: uma versão publicada nunca muda, o que é o ponto.

Descobrir por que uma proposta foi negada

O objetivo é sair de "esse cliente foi negado" até a tabela exata que o negou, em três consultas.

flowchart LR
  A["Execuções da janela de tempo"] --> B["Entrada e saída daquela execução"]
  B --> C["XML DMN da versão que rodou"]

Passo 1. Ache a execução pelo intervalo de tempo:

bash
curl -s "$BASE/decisions/$DECISION_ID/execution-logs?filter[from]=2026-08-01" \
  -H "Authorization: Bearer $TOKEN" | jq '.data[] | {id, versionId: .attributes.versionId}'
curl -s "$BASE/decisions/$DECISION_ID/execution-logs?filter[from]=2026-08-01" \
  -H "Authorization: Bearer $TOKEN" | jq '.data[] | {id, versionId: .attributes.versionId}'
json
{ "id": "1b2c...", "versionId": "8f1c..." }
{ "id": "1b2c...", "versionId": "8f1c..." }

Passo 2. Leia entrada e saída daquela execução:

bash
curl -s "$BASE/execution-logs/$LOG_ID" -H "Authorization: Bearer $TOKEN" \
  | jq '.data.attributes | {input, output, executionTimeMs}'
curl -s "$BASE/execution-logs/$LOG_ID" -H "Authorization: Bearer $TOKEN" \
  | jq '.data.attributes | {input, output, executionTimeMs}'

Passo 3. Recupere a tabela que rodou naquele momento:

bash
curl -s "$BASE/versions/$VERSION_ID" -H "Authorization: Bearer $TOKEN" | jq -r '.content'
curl -s "$BASE/versions/$VERSION_ID" -H "Authorization: Bearer $TOKEN" | jq -r '.content'

A saída é o XML DMN inteiro — a mesma tabela que avaliou a proposta, legível e anexável a um processo.

Armadilhas. O output gravado por /decisions/:id/execute tem a forma {"result": ..., "trace": [...]}, enquanto o gravado por /versions/:id/execute guarda a saída direta, sem envelope. Se você consome os logs programaticamente, trate as duas formas. E lembre que execução que falhou não tem log — a ausência de registro não significa que ninguém chamou.

Exportar todas as suas regras

Útil em auditoria, em backup e antes de qualquer conversa sobre migração. O laço abaixo percorre as decisões da organização, e dentro de cada uma as versões publicadas, gravando um .dmn por versão:

bash
for D in $(curl -s "$BASE/decisions?page[size]=100" -H "Authorization: Bearer $TOKEN" \
             | jq -r '.data[].id'); do
  for V in $(curl -s "$BASE/decisions/$D/versions?filter[status]=PUBLISHED" \
               -H "Authorization: Bearer $TOKEN" | jq -r '.data[].id'); do
    curl -s "$BASE/versions/$V" -H "Authorization: Bearer $TOKEN" \
      | jq -r '.content' > "dmn/${D}_${V}.dmn"
  done
done
for D in $(curl -s "$BASE/decisions?page[size]=100" -H "Authorization: Bearer $TOKEN" \
             | jq -r '.data[].id'); do
  for V in $(curl -s "$BASE/decisions/$D/versions?filter[status]=PUBLISHED" \
               -H "Authorization: Bearer $TOKEN" | jq -r '.data[].id'); do
    curl -s "$BASE/versions/$V" -H "Authorization: Bearer $TOKEN" \
      | jq -r '.content' > "dmn/${D}_${V}.dmn"
  done
done

Ao final, o diretório dmn/ tem um arquivo por versão publicada, nomeado {decisionId}_{versionId}.dmn.

Armadilhas. O XML sai íntegro e é DMN padrão — ele abre no Camunda Modeler e roda em qualquer motor conforme. Paginação padrão é 20 itens; passe page[size] até 100.

Modelar a tabela numa interface visual

O Decision Engine não tem editor gráfico (§15). O caminho prático:

  1. Modele a tabela no Camunda Modeler (gratuito para desenho) ou em qualquer editor DMN.
  2. Exporte o .dmn.
  3. Suba o arquivo pela rota de criação de versão, como no início rápido.

Armadilhas. Use o namespace do exemplo (https://www.omg.org/spec/DMN/20191111/MODEL/) — é o que o runner espera. Recursos DMN de nível de conformidade mais alto, como decisões encadeadas dentro do mesmo arquivo, dependem do runner e não estão cobertos por teste nosso: valide criando a versão, porque a validação acontece ali.


12

Integração com outros building blocks

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

PerguntaSe a resposta é sim
Eu já tenho todos os dados de entrada em mãos?Decision Engine. Você manda o input completo e recebe a resposta.
Preciso que alguém busque dados antes de decidir (bureau, arquivo, fila)?Decision Platform.
Preciso de resposta imediata e nada mais?Decision Engine.
Preciso de execução assíncrona, timeout configurável ou callback?Decision Platform.
Quem escreve e versiona a tabela de regras?Decision Engine. Sempre. A Platform não tem regras próprias.

Em uma frase: o Decision Engine é a regra; o Decision Platform é a esteira que leva os dados até ela. A Platform não substitui o Engine — ela o consome. Toda execução de esteira termina numa tabela DMN que vive aqui.

Building blockComo se relacionaObrigatório
IAMEmite o token; todas as rotas exigem organizationId e permissãoSim
Decision PlatformUsa este motor como avaliador; lê a última versão publicada da decisão apontada pela configNão
Feature FlagsResolve pertencimento a segmento avaliando DMN pela facade DecisionEngineFacadeTokenNão
Pricing EngineComplementar: a decisão diz se aprova, a precificação diz a que preçoNão
Calculations EngineComplementar: amortização, IOF e CET depois que a decisão aprovouNão
Audit TrailRegistra quem publicou qual versão; o ExecutionLog registra o que rodouNão
Webhooks EnginePode entregar os eventos decision-engine.* a sistemas externosNão
flowchart TD
  AN["Analista de risco"] -->|"escreve a tabela"| DE

  subgraph DE["Decision Engine — a REGRA"]
    P["Project"] --> D["Decision"] --> V["DecisionVersion com XML DMN em semver"]
    V --> PX["DmnProxyService"]
  end

  PX -->|"HTTP"| RU["runner DMN — Camunda DMN"]

  DP["Decision Platform — busca os dados e chama a regra"] -->|"consome como avaliador"| DE
  FF["Feature Flags"] -->|"avalia DMN de segmento pela facade"| DE

A seta que importa comercialmente é a primeira: quem escreve a regra é o analista de negócio, e não o desenvolvedor. É o mesmo XML em toda a cadeia.


13

Configuração e operação

Variáveis de ambiente

VariávelDescriçãoObrigatóriaPadrão
DMN_ENGINE_URLURL do runner DMN. O serviço não avalia nada sem ela.Sim (na prática)http://localhost:8080
DATABASE_URLPostgreSQL. O schema é decisions.Sim—
JWT_SECRETSegredo HS256 do IAM, mínimo 44 caracteresSim—
PORTPorta no modo standaloneNão3000 (mapeada para 3005)
DEPLOYMENT_MODEmonolith ou standaloneNãomonolith
MODULE_SELFIdentificação do serviço nos health checksNãodecision-engine

Dependências de infraestrutura

DependênciaPara quê
PostgreSQLSchema decisions — projetos, decisões, versões, tags, logs
Runner DMNAvaliação das tabelas. Imagem decision-engine-runner, exposta em 8081:8080 no compose local
IAMEmissão e verificação do token

O Redis não é dependência deste building block.

Limites

LimiteValor
Nome de projeto, decisão255 caracteres
Chave (key) de projeto e decisão100 caracteres, ^[a-z0-9-]+$, única por organização
Descrição1000 caracteres
Nome de tag100 caracteres, único por organização
Cor de tagHexadecimal de 6 dígitos, #RRGGBB
Formato da versãoSemver estrito \d+\.\d+\.\d+, único por decisão
Tamanho do XML DMNSem limite na aplicação; limitado pelo corpo HTTP e pela coluna text
Página padrão / máxima20 / 100 itens

Catálogo de erros

StatusCódigoSignificaO que fazer
400VALIDATIONCorpo reprovado no Zod (semver, chave, cor, UUID)Confira o formato contra §9
400VALIDATIONDMN execution failed: ...O runner recusou o XML ou a entrada. Valide o DMN e confira se as chaves do input casam com os inputExpression
400VALIDATIONInvalid DMN content na criação de versãoO XML não passou na validação. Abra no Camunda Modeler
400VALIDATIONDecision is not activePATCH na decisão com {"status":"ACTIVE"}
400VALIDATIONNo published version availablePublique uma versão
400VALIDATIONOnly published versions can be executedPublique a versão, ou use a rota da decisão
403—Organization context requiredAutentique informando a organização
403FORBIDDENPermissão faltando, ou o recurso é de outra organizaçãoConfira a permissão exata na tabela de §9
404NOT_FOUNDRecurso inexistente, de outra organização ou excluído logicamenteConfira o ID
409CONFLICTkey já existe, ou versão já existe, ou já publicada/arquivadaEscolha outra chave, ou confira o estado atual
500INTERNALFailed to publish eventO barramento de eventos falhou. A escrita principal já aconteceu

Observabilidade.

  • GET /decision-engine/health responde com nome e versão do build. É uma sonda de vida do processo — ela não verifica o runner DMN nem o banco. Para saber se o runner está de pé, use o /health do próprio runner (DMN_ENGINE_URL).
  • O DmnProxyService loga cada execução em nível debug com tempo e decisionId, e cada falha em error com a mensagem do runner. É por aí que se monitora latência do motor.
  • A tabela decisions.execution_logs é a fonte de verdade para volume, latência (executionTimeMs) e distribuição de versões em uso. Ela só registra sucesso (§15).
  • Eventos publicados: decision-engine.project.{created,updated,deleted}, decision-engine.decision.{created,updated,deleted,executed}, decision-engine.version.{created,published,archived}, decision-engine.tag.{created,updated,deleted}.

Os mesmos eventos, agrupados por recurso:

RecursoEventos
Projetodecision-engine.project.created · .updated · .deleted
Decisãodecision-engine.decision.created · .updated · .deleted · .executed
Versãodecision-engine.version.created · .published · .archived
Tagdecision-engine.tag.created · .updated · .deleted

14

Segurança e compliance

Isolamento entre tenants

Toda rota aplica o requireOrganization local, que devolve 403 quando o token não traz organizationId. O organizationId nunca é lido do corpo da requisição — ele vem do claim assinado.

flowchart TD
  T["Bearer JWT do IAM"] --> A["authMiddleware valida a assinatura"]
  A --> P["requirePermission verifica a permissão da rota"]
  P --> O{"O token traz organizationId?"}
  O -->|"não"| F403["403 Organization context required"]
  O -->|"sim"| S["Serviço recebe o organizationId do claim, nunca do corpo"]
  S --> R["Repositório filtra por organização"]

No acesso a dados, DecisionProject, Decision e DecisionTag carregam a coluna organization_id e os repositórios filtram por ela em toda consulta. Os modelos se dividem, então, em dois grupos:

ModeloComo é isolado
DecisionProjectColuna organization_id; o repositório filtra por ela em toda consulta
DecisionColuna organization_id; o repositório filtra por ela em toda consulta
DecisionTagColuna organization_id; o repositório filtra por ela em toda consulta
DecisionVersionSem a coluna — isolamento pela decisão dona
ExecutionLogColunas organization_id e subaccount_id, gravadas com o dono da decisão

DecisionVersion não tem a coluna: o isolamento dela vem da decisão dona. Fora do escopo, a resposta é 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 o motor como SaaS atende cada cliente como uma subconta dentro da própria organização. Aí o organizationId sozinho não separa mais nada, porque todos os clientes o compartilham: a fronteira passa a ser a subconta, resolvida pela chave de API presa a ela ou pelo header X-Subaccount-Id da chave da organização.

O queRegra
Quem vê o quêA subconta vê só o que é dela; a organização vê tudo. Vale para projeto, política, versão, tag e log de execução.
Chave e nomekey de projeto e de política, e name de tag, são únicos por dono. Dois clientes podem ter, cada um, a política credito-pf.
Projeto e políticaUma política nasce no projeto do mesmo dono. A organização não planta política própria dentro do projeto de um cliente.
TagsSó se liga tag do dono da política. Antes de 22/09/2026 o tagIds ia direto para a tabela de ligação, e uma tag de qualquer organização podia ser anexada pelo UUID.
Log de execuçãoGravado com o dono da política, mesmo quando foi a organização que executou. O consumo pertence a quem é dono.
Teste e produçãoO log guarda o ambiente da chave que executou. Execução de teste não gasta franquia e nunca é cobrada.
CotaExecutar a política direto gasta uma decisão como executar pela esteira: os dois caminhos contam na mesma franquia, e o excesso responde 402. Contar só a esteira deixaria este caminho como produção ilimitada e de graça.
EventosLevam metadata.subaccountId do dono do recurso, que é como o Webhooks Engine entrega ao cliente certo.

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, que cobre os dois building blocks: dois clientes montam a mesma coisa, com as mesmas chaves, num Postgres real, e cada um tenta todas as portas do outro.

O runner DMN não guarda nada

Cada execução envia o XML e a entrada no corpo da requisição. O runner é stateless: não há modelo implantado, não há catálogo e não há sessão. Isso significa que ele não é um ponto onde dados de um cliente possam vazar para outro — porque ele não retém dados de cliente nenhum.

Dados sensíveis no log de execução

O ExecutionLog grava a entrada completa da decisão em JSON. Numa esteira de crédito isso inclui, tipicamente, CPF, renda e score — dado pessoal sob a LGPD.

Atenção. O conteúdo não é criptografado em coluna, não há política de retenção automática e não há mascaramento (§15).

Trate o schema decisions como base que contém dado pessoal: controle de acesso ao banco, criptografia em repouso no volume e um processo de expurgo definido por você.

Explicabilidade da decisão automatizada

O art. 20 da LGPD assegura ao titular o direito de solicitar revisão de decisões tomadas unicamente com base em tratamento automatizado que afetem seus interesses — incluindo, expressamente, as que definem perfil de crédito. O ExecutionLog com versionId, entrada, saída e trace das regras avaliadas é a base factual para atender a esse pedido: ele diz qual tabela rodou, com que dados e qual linha disparou. A conformidade não é automática — o processo de revisão é seu —, mas o registro que ele exige existe.

Autenticação e permissões

Bearer JWT emitido pelo IAM. As permissões usadas são DECISION_PROJECTS_{CREATE,READ,UPDATE,DELETE}, DECISION_VERSIONS_{CREATE,READ,PUBLISH}, DECISION_TAGS_MANAGE, DECISION_LOGS_READ e DECISION_EXECUTE. Separe DECISION_VERSIONS_PUBLISH do resto ao desenhar papéis: publicar é o ato que muda a política em produção, e deve exigir mais gente do que escrever um rascunho.

Exclusão lógica

Projetos, decisões, versões e tags usam deletedAt. A linha permanece para que o log de execução continue apontando para uma versão existente. Atender a um pedido de eliminação sob a LGPD exige processo explícito de expurgo, hoje não automatizado.


15

Limitações conhecidas

LimitaçãoImpactoSituação
Execução com falha não gera ExecutionLogA coluna error existe na tabela mas nunca é preenchida: o log só é criado depois do sucesso do runner. Taxa de erro e latência de falha não são observáveis pela tabela.Lacuna conhecida — use o log da aplicação
Sem editor visual de tabela DMNO XML entra por API. Quem desenha usa uma ferramenta externa, como o Camunda Modeler.Por design hoje; interface no roadmap
Sem simulação nem teste em loteNão há endpoint para rodar uma versão contra um conjunto de casos e comparar com a anterior. A alternativa é chamar /versions/:id/execute em laço.Roadmap
Publicar já vale em produçãoNão há ambiente de ensaio dentro do building block, nem estágio entre DRAFT e valer.Por design — use organização de staging
Publicar não despublica a anteriorVárias versões ficam PUBLISHED e o motor usa a mais recente. Voltar atrás é arquivar a nova.Por design; pode surpreender
Sem retenção automática do log de execuçãoexecution_logs cresce indefinidamente, com entrada e saída em JSON. Em volume alto isso é custo de armazenamento e exposição de dado pessoal.Roadmap
/health não verifica o runner nem o bancoA sonda responde 200 com o serviço de pé mesmo com o runner DMN fora. Um orquestrador confiando só nela não detecta a falha.Lacuna conhecida
Permissões não seguem o recursoCriar decisão exige DECISION_VERSIONS_CREATE; excluir decisão exige DECISION_PROJECTS_DELETE; arquivar versão exige DECISION_VERSIONS_CREATE; ler tag exige DECISION_TAGS_MANAGE.Conceda papéis pela tabela de §9
DECISION_VERSIONS_UPDATE e DECISION_VERSIONS_DELETE não são usadasExistem no vocabulário do IAM, mas nenhuma rota deste building block as exige.Vocabulário à frente do código
executionLogsRouter montado em dois prefixosOs mesmos handlers respondem sob caminhos extras além dos três documentados.Use apenas as rotas de §9
Sem métricas nativas de negócioDistribuição de resultados, taxa de aprovação e uso por versão saem de consulta ao banco, não de um endpoint.Roadmap
trace só na rota da decisão/versions/:id/execute devolve apenas output e executionTimeMs.Por design
Sem cache de execuçãoEntradas idênticas repetidas vão ao runner todas as vezes.Por design — regra determinística com dado que muda

16

Perguntas frequentes

Qual é a diferença entre o Decision Engine e o Decision Platform, em uma frase?

O Decision Engine é a regra; o Decision Platform é a esteira que busca os dados e leva até a regra. Se você já tem todos os dados de entrada, chame o Engine direto. 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 que vive aqui de qualquer jeito.

flowchart LR
  Q{"Você já tem todos os dados de entrada em mãos?"}
  Q -->|"sim"| DE["Decision Engine — manda o input e recebe a resposta"]
  Q -->|"não, alguém precisa buscar antes"| DP["Decision Platform — a esteira"]
  DP -->|"termina chamando"| DE

Preciso saber DMN para usar?

Precisa entender o que é uma tabela de decisão, o que é fácil. Escrever o XML na mão é chato, e não é o caminho recomendado: modele no Camunda Modeler ou em outro editor DMN, exporte o .dmn e suba o arquivo. A recompensa de aprender o formato é que ele é padrão da OMG e a sua regra não fica presa a nenhum fornecedor, incluindo nós.

Se eu publicar uma versão errada, como volto atrás?

Arquivando a versão errada, com POST /versions/:versionId/archive. A anterior volta a ser a mais recente publicada automaticamente. Não tente republicar a antiga — ela já está PUBLISHED e o serviço responde 409.

Por que a minha decisão retorna 400 mesmo com a versão publicada?

Quase sempre porque a Decision está DRAFT. São dois interruptores independentes: a versão precisa estar PUBLISHED e a decisão precisa estar ACTIVE. Faça PATCH /decisions/:decisionId com {"status":"ACTIVE"}.

Como respondo a um pedido de revisão de decisão automatizada sob a LGPD?

Pelo ExecutionLog. Ele guarda a entrada exata, a saída, o trace das regras avaliadas e o versionId. Com o versionId, GET /versions/:versionId devolve o XML DMN que rodou — a tabela em si, legível, não uma reconstrução a partir de código. O registro existe; o processo de revisão humana é responsabilidade sua.

As minhas regras ficam presas na Catalisa?

Não. O que armazenamos em DecisionVersion.content é o XML DMN cru, e GET /versions/:versionId devolve ele inteiro. A receita de exportação em massa está em §11. Sair daqui custa o tempo de baixar os arquivos.

Qual motor DMN roda por trás?

Um runner baseado em Camunda DMN, empacotado como a imagem decision-engine-runner e mantido em repositório próprio. Ele é alcançado por HTTP através do DmnProxyService, com três operações. Isso torna o motor substituível sem impacto nas rotas — e é por isso que a documentação fala em "runner DMN" e não no nome de um fornecedor.

Quanto tempo demora uma execução?

Depende do tamanho da tabela e da latência até o runner, e o número que importa é o seu. Cada resposta traz executionTimeMs e cada ExecutionLog grava o valor, então medir o seu p95 é uma consulta ao banco. Não publicamos número de laboratório porque ele não sobrevive ao seu ambiente.

Dá para encadear decisões, com uma alimentando a outra?

Dentro de um mesmo arquivo DMN, isso depende do nível de conformidade do runner e não está coberto pelos nossos testes — valide criando a versão, já que a validação acontece ali. Entre decisões separadas, o building block não encadeia: quem orquestra sequência é o Decision Platform, e mesmo ele executa uma decisão por config.

Posso usar isto fora de crédito?

Pode, e faz sentido em qualquer lugar onde a regra muda mais rápido que o software: aceitação de seguro, elegibilidade em saúde, aprovação de desconto no varejo, roteamento de atendimento. O building block não sabe o que é crédito — ele avalia tabelas.


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