Decision Engine
ProduçãoRegras de negócio em tabelas DMN, versionadas e alteradas sem deploy
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.
- 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
- 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
- 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
26 endpoints em 6 recursos.
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.
| Atributo | Valor |
|---|---|
| Identificador | decision-engine |
| Categoria | Decisão |
| Escopo | Tenant (exige organizationId no token) |
| Porta (standalone) | 3005 |
| Path alias | @decision-engine |
| Prefixo HTTP | /decision-engine |
| Schema no banco | decisions |
| Status | Produção desde 2026-03 |
| Depende de | PostgreSQL, runner DMN (DMN_ENGINE_URL), IAM |
O problema
negócioO 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
ifaninhado 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.
Proposta de valor
negócio| Antes | Depois |
|---|---|
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 deploy | Mudar a política é POST .../versions + POST .../publish |
| "Qual regra rodou naquele dia?" é arqueologia de Git | Toda execução grava versão, entrada, saída e tempo |
| A regra nova substitui a antiga e some | Versões coexistem: rascunho, publicada e arquivada |
| Trocar de motor significa reescrever tudo | O 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.
Casos de uso reais
negócioCaso 1 — Uma financeira aperta o critério de risco na sexta e a regra vale na sexta Cenário ilustrativo
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 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 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 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
Operação de crédito consignado que precisa responder a uma reclamação formal sobre negativa de proposta.
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.
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.
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çãoCaso 3 — Uma seguradora troca regra de aceitação por produto sem multiplicar código Cenário ilustrativo
Seguradora com 12 produtos, cada um com sua tabela de aceitação e seus critérios de recusa automática.
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.
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.
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 --> FCaso 4 — Por que um padrão aberto importa na hora de trocar de fornecedor Referência de mercado
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.
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.
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 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"]
Mercado e diferenciais
negócioPanorama. 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ério | Catalisa Decision Engine | Camunda | Drools (Apache KIE) | IBM ODM | json-rules-engine |
|---|---|---|---|---|---|
| Formato da regra | DMN (XML da OMG) | DMN (XML da OMG) | DRL e DMN | Proprietário e DMN | JSON próprio |
| Preço | Precificação em definição | Cotação (tabela não publica valores) | Licença zero, você opera | Cotação, não público | Gratuito (ISC) |
| Versionamento e publicação | Nativo, com semver e estados | Via deploy de definição | Você constrói | Nativo | Não existe |
| Log por execução | Nativo, com versão usada | Via histórico da plataforma | Você constrói | Nativo | Não existe |
| Multi-tenant | Nativo, por organizationId do token | Você modela | Você constrói | Você modela | Não existe |
| Autorização por permissão | Nativa, vocabulário compartilhado | Externa | Externa | Nativa | Não existe |
| Modelador visual de tabela | Não (ver §15) | Sim, referência de mercado | Parcial | Sim | Não |
| Simulação e teste em lote | Não (ver §15) | Sim | Parcial | Sim | Não |
| Você opera o motor | Não | Sim, salvo SaaS | Sim | Sim | N/A |
Nossos diferenciais
- 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.
- O tenant é do token, não do modelo de dados. Toda rota exige
organizationIdno JWT e todo repositório filtra por ele. Num motor genérico, separar clientes é modelagem sua, e modelagem sua é onde vaza. - 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.
- O motor é substituível por trás. O
DmnProxyServicefala 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.
Modelo de cobrança e ROI
negócioUnidade de cobrança. Precificação em definição. Quando definida, a unidade natural é a execução de decisão — é o que o cliente entende, é o que escala com o uso e é o que aparece no ExecutionLog.
O que dispara custo.
| Driver | Por que ele importa |
|---|---|
| Execuções por mês | É a chamada ao runner DMN, o custo variável real |
| Decisões publicadas | Proxy do tamanho da operação de política |
| Retenção do log de execução | Cada 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 Engine | Camunda Self-Managed | Drools (Apache KIE) | IBM ODM | |
|---|---|---|---|---|
| Licença | Precificação em definição | Cotação — a tabela pública não publica valores | Zero (Apache 2.0) | Cotação, não público |
| Quem opera o motor | Catalisa | Você | Você | Você ou parceiro |
| Versionamento e log | Incluído | Você constrói | Você constrói | Incluído |
| Multi-tenant | Incluído | Você modela | Você constrói | Você 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 retorno | O que ele evita |
|---|---|
| O que você não constrói | Versionamento, publicação, log por execução e multi-tenant sobre um motor cru |
| O intervalo entre decidir e valer | Originaçã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.
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"]
endOs cinco routers e o que cada um monta:
| Prefixo montado | Router | Rotas |
|---|---|---|
/api/v1/projects | projectsRouter | 5 |
/api/v1/decisions | decisionsRouter | 6 |
/api/v1 | versionsRouter | 6 |
/api/v1/tags | tagsRouter | 5 |
/api/v1/execution-logs | executionLogsRouter | 3 |
/health | — | sonda simples |
E o que cada serviço faz:
| Serviço | Responsabilidade |
|---|---|
ProjectService | Agrupamento de decisões |
DecisionService | Ciclo de vida + execute (usa a última PUBLISHED) |
VersionService | Semver, publish, archive, execute de versão fixa |
TagService | Rótulos por organização |
ExecutionLogService | Consulta 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 versionUsedAs mesmas seis etapas, com o que reprova cada uma:
| # | Etapa | Se falhar |
|---|---|---|
| 1 | Decision existe nesta organização? | 404 |
| 2 | Decision.status == ACTIVE? | 400 |
| 3 | Existe DecisionVersion PUBLISHED? Pega a mais recente publicada | 400 |
| 4 | DmnProxyService.execute(version.content, input) → POST $DMN_ENGINE_URL/api/v1/execute | 400 com a mensagem do runner |
| 5 | Grava ExecutionLog (entrada, saída, trace, tempo, quem) | — |
| 6 | Publica o evento decision-engine.decision.executed | 500 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.
| Rota | Propósito | Versão usada | Devolve trace? |
|---|---|---|---|
POST /decisions/:id/execute | Produção — o chamador não sabe nem quer saber a versão | A PUBLISHED mais recente | Sim |
POST /versions/:id/execute | Teste — você escolhe a versão de propósito | A que você indicar | Nã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.
Conceitos e modelo de dados
Glossário
| Termo | Significa |
|---|---|
| DMN | Decision Model and Notation, padrão da OMG para modelar decisões. A regra é um XML. |
| FEEL | Friendly Enough Expression Language, a linguagem de expressão do DMN. É o que você escreve numa célula: >= 18, "adult". |
| Tabela de decisão | O <decisionTable> do DMN: colunas de entrada, colunas de saída, uma linha por regra. |
| Hit policy | Como o motor resolve quando mais de uma linha casa. FIRST devolve a primeira; há outras no padrão. |
| Project | Agrupamento de decisões. Existe para organizar, não para isolar — o isolamento é por organização. |
| Decision | A decisão de negócio, com chave estável (politica-credito-pessoal). É o que o seu aplicativo referencia. |
| DecisionVersion | Uma versão do XML DMN daquela decisão, em semver. É o que de fato executa. |
| Tag | Rótulo colorido por organização, para cortar o catálogo de decisões. |
| ExecutionLog | O registro de uma execução: entrada, saída, tempo, quem executou e qual versão. |
Modelo de dados — schema decisions no PostgreSQL.
| Modelo Prisma | Tabela | Propósito | Campos-chave |
|---|---|---|---|
DecisionProject | decisions.decision_projects | Agrupa decisões | key único por organização, ownerId, deletedAt |
Decision | decisions.decisions | A decisão de negócio | key único por organização, projectId, status, deletedAt |
DecisionVersion | decisions.decision_versions | O XML DMN versionado | content (texto), version único por decisão, status, publishedAt |
DecisionTag | decisions.decision_tags | Rótulo por organização | name único por organização, color (hex) |
DecisionTagLink | decisions.decision_tag_links | Liga decisão e tag | Chave composta (decisionId, tagId) |
ExecutionLog | decisions.execution_logs | Rastro de execução | decisionId, versionId, input, output, executionTimeMs, executedBy |
Enumerações
| Enum | Valores |
|---|---|
DecisionStatus | DRAFT · ACTIVE · ARCHIVED |
VersionStatus | DRAFT · 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 noteAtençã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ção | Endpoint |
|---|---|
criação → DRAFT | POST /decision-engine/api/v1/decisions/:decisionId/versions |
DRAFT → PUBLISHED | POST /decision-engine/api/v1/versions/:versionId/publish |
DRAFT ou PUBLISHED → ARCHIVED | POST /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.
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étodo | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /decision-engine/api/v1/projects | Cria projeto | DECISION_PROJECTS_CREATE |
GET | /decision-engine/api/v1/projects | Lista projetos, paginado | DECISION_PROJECTS_READ |
GET | /decision-engine/api/v1/projects/:projectId | Busca projeto | DECISION_PROJECTS_READ |
PATCH | /decision-engine/api/v1/projects/:projectId | Atualiza nome e descrição | DECISION_PROJECTS_UPDATE |
DELETE | /decision-engine/api/v1/projects/:projectId | Exclusão lógica. Responde 204 | DECISION_PROJECTS_DELETE |
Decisões — /decision-engine/api/v1/decisions
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /decision-engine/api/v1/decisions | Cria decisão em DRAFT | DECISION_VERSIONS_CREATE |
GET | /decision-engine/api/v1/decisions | Lista decisões, paginado | DECISION_VERSIONS_READ |
GET | /decision-engine/api/v1/decisions/:decisionId | Busca decisão | DECISION_VERSIONS_READ |
PATCH | /decision-engine/api/v1/decisions/:decisionId | Atualiza nome, descrição, status e tags | DECISION_VERSIONS_CREATE |
DELETE | /decision-engine/api/v1/decisions/:decisionId | Exclusão lógica. Responde 204 | DECISION_PROJECTS_DELETE |
POST | /decision-engine/api/v1/decisions/:decisionId/execute | Executa a última versão publicada | DECISION_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_CREATEe que excluir exigeDECISION_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étodo | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /decision-engine/api/v1/decisions/:decisionId/versions | Cria versão. Valida o DMN antes de gravar | DECISION_VERSIONS_CREATE |
GET | /decision-engine/api/v1/decisions/:decisionId/versions | Lista versões da decisão | DECISION_VERSIONS_READ |
GET | /decision-engine/api/v1/versions/:versionId | Busca versão. Devolve o XML DMN em content | DECISION_VERSIONS_READ |
POST | /decision-engine/api/v1/versions/:versionId/publish | Publica a versão | DECISION_VERSIONS_PUBLISH |
POST | /decision-engine/api/v1/versions/:versionId/archive | Arquiva a versão | DECISION_VERSIONS_CREATE |
POST | /decision-engine/api/v1/versions/:versionId/execute | Executa esta versão específica | DECISION_EXECUTE |
Filtro de listagem: filter[status] (DRAFT, PUBLISHED, ARCHIVED).
Tags — /decision-engine/api/v1/tags
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /decision-engine/api/v1/tags | Cria tag | DECISION_TAGS_MANAGE |
GET | /decision-engine/api/v1/tags | Lista tags, paginado | DECISION_TAGS_MANAGE |
GET | /decision-engine/api/v1/tags/:tagId | Busca tag | DECISION_TAGS_MANAGE |
PATCH | /decision-engine/api/v1/tags/:tagId | Atualiza nome e cor | DECISION_TAGS_MANAGE |
DELETE | /decision-engine/api/v1/tags/:tagId | Remove tag. Responde 204 | DECISION_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étodo | Rota | Descrição | Permissão |
|---|---|---|---|
GET | /decision-engine/api/v1/execution-logs | Lista todos os logs da organização | DECISION_LOGS_READ |
GET | /decision-engine/api/v1/execution-logs/:logId | Busca um log | DECISION_LOGS_READ |
GET | /decision-engine/api/v1/decisions/:decisionId/execution-logs | Logs de uma decisão | DECISION_LOGS_READ |
Filtros: filter[decisionId], filter[versionId] (UUID), filter[from] e filter[to] (data ISO-8601).
O
executionLogsRouterestá montado em dois pontos (/api/v1/execution-logse/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étodo | Rota | Descrição |
|---|---|---|
GET | /decision-engine/health | Sonda 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
{
"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>"
}| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
version | string | Sim | Semver estrito, no formato \d+\.\d+\.\d+. Único por decisão. |
content | string | Sim | O XML DMN completo. Mínimo 1 caractere; validado no runner. |
Resposta 201
{
"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
| Status | Quando |
|---|---|
400 | version fora de semver, content vazio, ou DMN inválido segundo o runner |
403 | Token sem organizationId ou sem DECISION_VERSIONS_CREATE |
404 | A decisão não existe nesta organização |
409 | Já 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
{ "input": { "age": 25, "score": 730, "renda": 5200 } }{ "input": { "age": 25, "score": 730, "renda": 5200 } }| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
input | object | Sim | Mapa livre de variáveis. As chaves precisam casar com os inputExpression da tabela DMN. |
Resposta 200
{
"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
| Status | Quando |
|---|---|
400 | Decision is not active — a decisão não está ACTIVE |
400 | No published version available — nenhuma versão publicada |
400 | DMN execution failed: ... — o runner recusou o XML ou a entrada |
403 | Token sem organizationId ou sem DECISION_EXECUTE |
404 | A 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
{ "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
| Status | Quando |
|---|---|
400 | Only published versions can be executed — a versão está DRAFT ou ARCHIVED |
403 | A versão pertence a outra organização, ou falta DECISION_EXECUTE |
404 | A versão não existe |
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
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/v1TOKEN=$(curl -s -X POST https://iam.bb.stg.catalisa.app/iam/api/v1/users/login \
-H "Content-Type: application/json" \
-d '{
"email": "admin@test.com",
"password": "password123",
"organizationId": "b0000000-0000-0000-0000-000000000001"
}' | jq -r .accessToken)
BASE=https://decision-engine.bb.stg.catalisa.app/decision-engine/api/v1Confira que o token chegou antes de seguir — sem ele, todos os passos seguintes respondem 401:
echo "${TOKEN:0:12}..."
# eyJhbGciOiJI...echo "${TOKEN:0:12}..."
# eyJhbGciOiJI...2. Criar o projeto
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.
echo "$PROJECT_ID"
# 7c9e6679-7425-40de-944b-e07fc1f90ae7echo "$PROJECT_ID"
# 7c9e6679-7425-40de-944b-e07fc1f90ae7A key precisa casar com ^[a-z0-9-]+$ e é única na sua organização.
3. Criar a decisão
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.
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:
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>>= 18</text></inputEntry>
<outputEntry id="oe1"><text>"adult"</text></outputEntry>
</rule>
<rule id="rule2">
<inputEntry id="ie2"><text>< 18</text></inputEntry>
<outputEntry id="oe2"><text>"minor"</text></outputEntry>
</rule>
</decisionTable>
</decision>
</definitions>
DMNcat > /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>>= 18</text></inputEntry>
<outputEntry id="oe1"><text>"adult"</text></outputEntry>
</rule>
<rule id="rule2">
<inputEntry id="ie2"><text>< 18</text></inputEntry>
<outputEntry id="oe2"><text>"minor"</text></outputEntry>
</rule>
</decisionTable>
</decision>
</definitions>
DMNAgora suba o arquivo como versão 1.0.0. O jq -Rs embrulha o XML inteiro como string JSON:
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 DRAFTVERSION_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 DRAFTSe 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:
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:
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
curl -s -X POST "$BASE/decisions/$DECISION_ID/execute" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"input":{"age":25}}' | jqcurl -s -X POST "$BASE/decisions/$DECISION_ID/execute" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"input":{"age":25}}' | jq{
"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
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:
{
"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.
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:
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:
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:
curl -s -X POST "$BASE/versions/$VERSION_ID/execute" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"input":{"age":25}}' | jqcurl -s -X POST "$BASE/versions/$VERSION_ID/execute" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"input":{"age":25}}' | jq{ "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:
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
PUBLISHEDao mesmo tempo. O motor usa a mais recente. O jeito de voltar atrás é arquivar a nova, não republicar a antiga — republicar retorna409, porque ela já está publicada. - Semver é obrigatório e imutável.
1.1é rejeitado; precisa ser1.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:
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}'{ "id": "1b2c...", "versionId": "8f1c..." }{ "id": "1b2c...", "versionId": "8f1c..." }Passo 2. Leia entrada e saída daquela execução:
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:
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:
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
donefor 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
doneAo 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:
- Modele a tabela no Camunda Modeler (gratuito para desenho) ou em qualquer editor DMN.
- Exporte o
.dmn. - 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.
Integração com outros building blocks
A fronteira que mais gera dúvida: Decision Engine ou Decision Platform?
| Pergunta | Se 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 block | Como se relaciona | Obrigatório |
|---|---|---|
| IAM | Emite o token; todas as rotas exigem organizationId e permissão | Sim |
| Decision Platform | Usa este motor como avaliador; lê a última versão publicada da decisão apontada pela config | Não |
| Feature Flags | Resolve pertencimento a segmento avaliando DMN pela facade DecisionEngineFacadeToken | Não |
| Pricing Engine | Complementar: a decisão diz se aprova, a precificação diz a que preço | Não |
| Calculations Engine | Complementar: amortização, IOF e CET depois que a decisão aprovou | Não |
| Audit Trail | Registra quem publicou qual versão; o ExecutionLog registra o que rodou | Não |
| Webhooks Engine | Pode entregar os eventos decision-engine.* a sistemas externos | Nã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"| DEA 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.
Configuração e operação
Variáveis de ambiente
| Variável | Descrição | Obrigatória | Padrão |
|---|---|---|---|
DMN_ENGINE_URL | URL do runner DMN. O serviço não avalia nada sem ela. | Sim (na prática) | http://localhost:8080 |
DATABASE_URL | PostgreSQL. O schema é decisions. | Sim | — |
JWT_SECRET | Segredo HS256 do IAM, mínimo 44 caracteres | Sim | — |
PORT | Porta no modo standalone | Não | 3000 (mapeada para 3005) |
DEPLOYMENT_MODE | monolith ou standalone | Não | monolith |
MODULE_SELF | Identificação do serviço nos health checks | Não | decision-engine |
Dependências de infraestrutura
| Dependência | Para quê |
|---|---|
| PostgreSQL | Schema decisions — projetos, decisões, versões, tags, logs |
| Runner DMN | Avaliação das tabelas. Imagem decision-engine-runner, exposta em 8081:8080 no compose local |
| IAM | Emissão e verificação do token |
O Redis não é dependência deste building block.
Limites
| Limite | Valor |
|---|---|
| Nome de projeto, decisão | 255 caracteres |
Chave (key) de projeto e decisão | 100 caracteres, ^[a-z0-9-]+$, única por organização |
| Descrição | 1000 caracteres |
| Nome de tag | 100 caracteres, único por organização |
| Cor de tag | Hexadecimal de 6 dígitos, #RRGGBB |
| Formato da versão | Semver estrito \d+\.\d+\.\d+, único por decisão |
| Tamanho do XML DMN | Sem limite na aplicação; limitado pelo corpo HTTP e pela coluna text |
| Página padrão / máxima | 20 / 100 itens |
Catálogo de erros
| Status | Código | Significa | O que fazer |
|---|---|---|---|
400 | VALIDATION | Corpo reprovado no Zod (semver, chave, cor, UUID) | Confira o formato contra §9 |
400 | VALIDATION | DMN execution failed: ... | O runner recusou o XML ou a entrada. Valide o DMN e confira se as chaves do input casam com os inputExpression |
400 | VALIDATION | Invalid DMN content na criação de versão | O XML não passou na validação. Abra no Camunda Modeler |
400 | VALIDATION | Decision is not active | PATCH na decisão com {"status":"ACTIVE"} |
400 | VALIDATION | No published version available | Publique uma versão |
400 | VALIDATION | Only published versions can be executed | Publique a versão, ou use a rota da decisão |
403 | — | Organization context required | Autentique informando a organização |
403 | FORBIDDEN | Permissão faltando, ou o recurso é de outra organização | Confira a permissão exata na tabela de §9 |
404 | NOT_FOUND | Recurso inexistente, de outra organização ou excluído logicamente | Confira o ID |
409 | CONFLICT | key já existe, ou versão já existe, ou já publicada/arquivada | Escolha outra chave, ou confira o estado atual |
500 | INTERNAL | Failed to publish event | O barramento de eventos falhou. A escrita principal já aconteceu |
Observabilidade.
GET /decision-engine/healthresponde 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/healthdo próprio runner (DMN_ENGINE_URL).- O
DmnProxyServiceloga cada execução em níveldebugcom tempo edecisionId, e cada falha emerrorcom 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:
| Recurso | Eventos |
|---|---|
| Projeto | decision-engine.project.created · .updated · .deleted |
| Decisão | decision-engine.decision.created · .updated · .deleted · .executed |
| Versão | decision-engine.version.created · .published · .archived |
| Tag | decision-engine.tag.created · .updated · .deleted |
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:
| Modelo | Como é isolado |
|---|---|
DecisionProject | Coluna organization_id; o repositório filtra por ela em toda consulta |
Decision | Coluna organization_id; o repositório filtra por ela em toda consulta |
DecisionTag | Coluna organization_id; o repositório filtra por ela em toda consulta |
DecisionVersion | Sem a coluna — isolamento pela decisão dona |
ExecutionLog | Colunas 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 que | Regra |
|---|---|
| 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 nome | key 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ítica | Uma política nasce no projeto do mesmo dono. A organização não planta política própria dentro do projeto de um cliente. |
| Tags | Só 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ção | Gravado com o dono da política, mesmo quando foi a organização que executou. O consumo pertence a quem é dono. |
| Teste e produção | O log guarda o ambiente da chave que executou. Execução de teste não gasta franquia e nunca é cobrada. |
| Cota | Executar 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. |
| Eventos | Levam 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.
Limitações conhecidas
| Limitação | Impacto | Situação |
|---|---|---|
Execução com falha não gera ExecutionLog | A 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 DMN | O 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 lote | Nã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ção | Nã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 anterior | Vá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ção | execution_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 banco | A 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 recurso | Criar 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 usadas | Existem no vocabulário do IAM, mas nenhuma rota deste building block as exige. | Vocabulário à frente do código |
executionLogsRouter montado em dois prefixos | Os mesmos handlers respondem sob caminhos extras além dos três documentados. | Use apenas as rotas de §9 |
| Sem métricas nativas de negócio | Distribuiçã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ção | Entradas idênticas repetidas vão ao runner todas as vezes. | Por design — regra determinística com dado que muda |
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"| DEPreciso 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