Decision models (DMN)
Decision Models is not yet available in a released build. The DMN parser and evaluator described on this page are implemented and tested, and a businessRuleTask inside a deployed BPMN process already evaluates decision tables through them — but the standalone decision-management API (creating, listing, and editing dmn_decision resources) is gated behind a feature flag that is off in every shipped image today, so every route for it returns 404. Nothing below is reachable through the product yet.
What DMN decision tables are
Section titled “What DMN decision tables are”A DMN 1.3 decision table is a spreadsheet-shaped set of rules: input columns, output columns, and rows where each cell is a condition or a result. Orvanta’s evaluator supports the five standard “single output” hit policies:
| Hit policy | Behaviour |
|---|---|
| UNIQUE | Exactly one rule may match; more than one match is an evaluation error. |
| FIRST | The first matching rule (in document order) wins. |
| PRIORITY | The matching rule with the highest declared output priority wins. |
| ANY | Multiple rules may match, but they must all produce the same output. |
| COLLECT | Every matching rule’s output is returned as a list. |
Cell expressions use FEEL — see the cell grammar below. Input/output values in Orvanta’s evaluator are scalars, temporal values, and lists only — a variable holding a JSON object is omitted from what a decision table can see rather than failing the evaluation; if the table references that name anyway, it resolves as the same null a missing key would produce, never a hard error.
COLLECT’s numeric aggregators are implemented, not just the plain list form: C+/aggregation="SUM", C</MIN, C>/MAX, and C#/COUNT all run, alongside plain C (every matched rule’s output, as a list). An aggregated COLLECT is defined over a single numeric output column only — a compound-output table under C+/C</C> is an evaluation error naming the column. Both spellings found in the wild are accepted (hitPolicy="C+" and hitPolicy="COLLECT" aggregation="SUM"); if a document sets both, they must agree. Orvanta pins two readings the spec leaves open, matching Camunda/Flowable/Drools rather than a literal reading of the spec text:
| Case | Orvanta’s answer |
|---|---|
C+/C# over repeated identical outputs (e.g. [10, 10, 5]) | Summed/counted as-is (25, 3) — not de-duplicated first. |
C+/C# with no matching rule | 0 (the additive/counting identity). |
C</C> with no matching rule | null — the minimum/maximum of nothing has no value. |
Plain C with no matching rule | The whole result is null (unchanged from before aggregators existed). |
A declared outputValues domain is enforced under every hit policy, not just documentation and not just PRIORITY’s ranking. DMN 1.3 §8.2.10 lets an output column declare the list of values it’s allowed to produce. Orvanta’s evaluator checks every candidate rule’s output against that list at the same point it applies a hit policy — a rule whose output falls outside a declared outputValues domain is rejected (OutputValueNotAllowed) rather than silently returned, under UNIQUE/FIRST/ANY/COLLECT exactly as much as under PRIORITY.
The FEEL-lite cell grammar
Section titled “The FEEL-lite cell grammar”Orvanta’s FEEL evaluator (orvanta-feel) is a hand-written, deliberately partial implementation — not a full FEEL engine. Input-entry (unary test) cells support:
- Bare-any:
-or an empty cell. - The literal
null(andnot(null)). - Literal equality: a bare
"gold",42,true, or an explicit= x. - Comma-separated lists as OR:
"NL","BE". - Comparisons
<,<=,>,>=over number, string, date, time, date-and-time, or duration. - Ranges
[1..10],(1..10), and mixed brackets[1..10)/(1..10], over the same set of types (["a".."m"),[date("2026-01-01")..date("2026-12-31")]). not(...)and comma lists inside it (not("NL","BE")).- The
inoperator (in [1..10],? in ("a","b")) — the implicit-input form?is only meaningful on the left ofin. and/orconjunction/disjunction between tests (andbinds tighter thanor).- A comparison or range bound can itself be a variable name resolved from the table’s own inputs (
< limit,[low..high]) instead of a literal.
Output-entry cells support number/string/boolean/temporal literals and simple arithmetic (+ - * /, unary -, parentheses) over identifiers.
Temporal literals: date("YYYY-MM-DD"), time("hh:mm:ss[.fff][Z|±hh:mm]"), date and time("YYYY-MM-DDThh:mm:ss[.fff][Z|±hh:mm]"), and duration("P…") (FEEL’s two distinct duration types — P1Y2M years-and-months, P1DT2H30M days-and-time — do not compare against each other). A column’s declared typeRef (string, number, boolean, date, time, date and time/dateTime, dayTimeDuration, yearMonthDuration) coerces an inbound value to that type before any rule runs — a JSON "100" reaching a typeRef="number" column matches >= 100 rather than erroring. An unrecognised or absent typeRef (including a custom itemDefinition) is Any: no coercion, no check.
Anything outside this subset — full FEEL contexts/functions/for-loops, qualified names like customer.tier on the left of a comparison, IANA zone identifiers (time("10:00:00@Europe/Paris")) — is a parse error, not a silent partial match.
Engine limits
Section titled “Engine limits”Hard caps enforced during parsing, before any rule runs:
| Limit | Value |
|---|---|
| DMN XML document size | 1 MiB |
| Rules per decision table | 2,000 |
| Input columns per table | 64 |
| Output columns per table | 64 |
| A single cell’s text | 4,096 bytes |
| A single FEEL entry’s parse/eval recursion depth | 64 |
A document over any of these limits fails to parse with an explicit error rather than being read partially.
Decision Services and DRDs
Section titled “Decision Services and DRDs”Beyond a single decision table, DMN lets decisions reference each other through a Decision Requirements Diagram (DRD) — <decision> elements linked by <informationRequirement>, with <decisionService> elements exposing a composed subset of them as one callable unit. Orvanta’s DMN engine parses and evaluates this graph — resolving <inputData>, requirement edges, and decision services, with explicit errors for a malformed graph (a dangling reference, a required-decision cycle, a decision missing the <variable> its result binds under, and so on).
None of this is reachable in a released build yet. Like the standalone decision-management API above, the DRD canvas and a dedicated decision-service evaluation route don’t ship until the end of the programme this belongs to (orvanta-platform#731) — the engine and route exist, but nothing in the product surfaces them.
One exception, already live today: if a businessRuleTask (or a Flowable serviceTask flowable:type="dmn") references a <decisionService> rather than a plain decision, Orvanta deploys it — with a warning — and evaluates only the single decision the service names. The surrounding DRD composition (multiple output decisions, encapsulated decisions feeding into them) is not honoured; only ever the one referenced decision’s own table runs.
orvanta:decisionRef
Section titled “orvanta:decisionRef”A businessRuleTask links to a decision table with the orvanta:decisionRef extension attribute, whose value is the path of a stored dmn_decision resource — not an inline document. At runtime the engine resolves that path once per process (before execution starts, alongside script bindings) and evaluates it synchronously and inline on the worker — no separate job is dispatched, the same way a gateway condition evaluates inline.
camunda:decisionRef and flowable:decisionRef are parsed but deliberately ignored — they never bind as an Orvanta decision reference. If you’re importing a Flowable or Camunda process with a business rule task, re-point it at an Orvanta orvanta:decisionRef after import; see Importing from Flowable/Camunda.
What a decision table can see
Section titled “What a decision table can see”A businessRuleTask evaluates its decision table against the merge of three sources, least to most specific (a later source wins a name collision):
- The process’s start payload (
flow_input) — its fields are readable under their own bare name, so a table authored against the start form’s field names (age,country,amount) reads them directly, and additionally and unshadowably under theflow_input.prefix (flow_input.age) — the same name a JavaScript input transform or gateway condition uses for the same value. results.<elementId>— upstream activity results.- The current scope’s named process variables — the more likely intentional input than a same-named activity result.
A JSON object flattens into dotted keys before this lookup ({"applicant": {"age": 35}} contributes applicant.age), and any value with no FEEL representation is omitted from the table’s inputs — logged by name and reason rather than dropped silently. This is exactly the same input mapping POST .../evaluate (and the DMN test panel, which calls it) uses, so a table that matches in a running process behaves identically when tested standalone.
See also
Section titled “See also”- Element reference:
businessRuleTaskin context with every other supported element. - Execution semantics: how a business rule task’s evaluation fits into the instance’s token model.
- Importing from Flowable/Camunda: re-pointing
camunda:decisionRef/flowable:decisionRefafter import. - The decision-table editor: the native Visual/XML editor and its test panel.
orvanta dmn: manage and evaluate decisions from the command line.- Build your first decision: a step-by-step tutorial covering a UNIQUE table, every hit policy, and wiring it into a BPMN process.