Decision Models (DMM)
Stateway includes a built-in decision engine that evaluates Decision Model (DMM) tables. Decisions can be evaluated standalone via the API or embedded in processes via businessRuleTask elements.
Overview
A decision model consists of:
- Inputs: Variables that drive the decision (e.g.,
amount,risk_score) - Outputs: Values produced by the decision (e.g.,
approved,discount) - Rules: Rows that map input conditions to output values
- Hit Policy: How multiple matching rules are handled
Creating a Decision Definition
JSON Format
The JSON source is a flat object with key, name, inputs, outputs, rules, and hitPolicy:
Request:
curl -X POST https://api.stateway.io/v1/decisions \
-H "X-API-Key: sw_live_your_key" \
-H "Content-Type: application/json" \
-d '{
"key": "discount-calculator",
"name": "Discount Calculator",
"source_type": "json",
"source": {
"key": "discount-calculator",
"name": "Discount Calculator",
"hitPolicy": "FIRST",
"inputs": [
{ "name": "customerType", "type": "string", "description": "Customer tier" },
{ "name": "orderTotal", "type": "number" }
],
"outputs": [
{ "name": "discountPercent", "type": "number", "defaultValue": 0 }
],
"rules": [
{ "id": "rule-1",
"conditions": {
"customerType": { "operator": "=", "value": "premium" },
"orderTotal": { "operator": ">=", "value": 1000 }
},
"outputs": { "discountPercent": 15 } },
{ "id": "rule-2",
"conditions": {
"customerType": { "operator": "=", "value": "premium" },
"orderTotal": { "operator": "<", "value": 1000 }
},
"outputs": { "discountPercent": 10 } },
{ "id": "rule-3",
"conditions": {
"customerType": { "operator": "=", "value": "standard" },
"orderTotal": { "operator": ">=", "value": 500 }
},
"outputs": { "discountPercent": 5 } },
{ "id": "rule-4", "conditions": {}, "outputs": { "discountPercent": 0 } }
]
}
}'
Each object in inputs / outputs needs a non-empty name and a type of string,
number, or boolean. description (inputs) and defaultValue (outputs) are optional; any
other field — including id and label — is ignored. The source object must repeat key
and also carry its own name (the envelope name is not copied into it).
A rule with "conditions": {} (no fields) always matches — use it as the catch-all, ordered
last under hitPolicy: FIRST.
Response:
{
"data": {
"id": "dmd_01j...",
"key": "discount-calculator",
"version": 1,
"name": "Discount Calculator",
"source_type": "json",
"source_hash": "b2e1f3a4...",
"is_active": true,
"created_at": "2026-04-26T12:00:00.000Z"
}
}
YAML Format
Request:
curl -X POST https://api.stateway.io/v1/decisions \
-H "X-API-Key: sw_live_your_key" \
-H "Content-Type: application/json" \
-d '{
"key": "discount-calculator",
"name": "Discount Calculator",
"source_type": "yaml",
"source": "key: discount-calculator\nname: Discount Calculator\nhitPolicy: FIRST\ninputs:\n - name: customerType\n type: string\n - name: orderTotal\n type: number\noutputs:\n - name: discountPercent\n type: number\n defaultValue: 0\nrules:\n - id: rule-1\n conditions:\n customerType: { operator: \"=\", value: premium }\n orderTotal: { operator: \">=\", value: 1000 }\n outputs: { discountPercent: 15 }\n - id: rule-4\n conditions: {}\n outputs: { discountPercent: 0 }\n"
}'
Expanded, that YAML source reads:
key: discount-calculator
name: Discount Calculator
hitPolicy: FIRST
inputs:
- name: customerType
type: string
- name: orderTotal
type: number
outputs:
- name: discountPercent
type: number
defaultValue: 0
rules:
- id: rule-1
conditions:
customerType: { operator: "=", value: premium }
orderTotal: { operator: ">=", value: 1000 }
outputs: { discountPercent: 15 }
- id: rule-4 # no conditions → catch-all
conditions: {}
outputs: { discountPercent: 0 }
Response:
{
"data": {
"id": "dmd_01j...",
"key": "discount-calculator",
"version": 1,
"name": "Discount Calculator",
"source_type": "yaml",
"source_hash": "b2e1f3a4...",
"is_active": true,
"created_at": "2026-04-26T12:00:00.000Z"
}
}
Hit Policies
| Policy | Result shape |
|---|---|
| FIRST | Output object of the first matching rule (rule order matters) |
| UNIQUE | Output object of the single matching rule (same shape as FIRST; multiple matches are not rejected today) |
| ALL | { "results": [ …output objects, in rule order ] } |
| RULE_ORDER | { "results": [ …output objects, in rule order ] } |
| COLLECT | { "results": [ …output objects ] } |
| OUTPUT_ORDER | Planned — currently behaves like ALL ({ "results": […] }); output-value ordering is not applied yet |
| PRIORITY | Planned — currently behaves like FIRST; output-priority selection is not applied yet |
When no rule matches, the result is the output defaults: { "<outputName>": <defaultValue ?? null>, … }.
Example: ALL Hit Policy
With hitPolicy: ALL, both rules below match for orderTotal: 1500:
{
"rules": [
{ "id": "r1", "conditions": { "orderTotal": { "operator": ">", "value": 1000 } }, "outputs": { "tier": "high" } },
{ "id": "r2", "conditions": { "orderTotal": { "operator": ">", "value": 500 } }, "outputs": { "tier": "medium" } }
]
}
The result is { "results": [ { "tier": "high" }, { "tier": "medium" } ] }.
Rule Conditions (json / yaml)
For json and yaml sources, rules[].conditions is an object keyed by input name. Each
entry is { "operator": …, "value": … }. Omit an input to mean "any value"; an empty
conditions object matches every input.
| Operator | Meaning | value |
|---|---|---|
= | equals | scalar |
!= | not equal | scalar |
> >= < <= | numeric comparison | number |
between | value ≤ input ≤ value2 (inclusive) | requires both value and value2 |
in | input is one of the listed values | array |
not_in | input is none of the listed values | array |
contains | String(input) contains String(value) | scalar |
"conditions": {
"customerType": { "operator": "in", "value": ["gold", "silver"] },
"orderTotal": { "operator": "between", "value": 500, "value2": 1000 }
}
Any other operator, a missing value, or conditions written as a positional array
(["premium", ">= 1000"]) is rejected at publish time with a 400.
FEEL Expression Conditions (dmn_xml only)
Positional FEEL conditions ("> 1000", ranges like "[500..1000]", value lists, NOT, and the
"-" wildcard) apply only to source_type: "dmn_xml" — standard OMG DMN 1.3 decision tables,
where each <inputEntry> holds one FEEL expression positionally per <input>. They are not
accepted in json / yaml sources; use the structured conditions object above instead.
Evaluating a Decision
Standalone Evaluation
Request:
curl -X POST https://api.stateway.io/v1/decisions/discount-calculator/evaluate \
-H "X-API-Key: sw_live_your_key" \
-H "Content-Type: application/json" \
-d '{
"variables": {
"customerType": "premium",
"orderTotal": 1500
}
}'
Response:
{
"data": {
"evaluation_id": "eval_01j...",
"decision_key": "discount-calculator",
"decision_version": 1,
"decision_hash": "b2e1f3a4...",
"matched": true,
"rules_matched": ["rule-1"],
"outputs": {
"discountPercent": 15
},
"evaluated_at": "2026-04-26T12:01:00.000Z"
}
}
The evaluation_id can be used to retrieve this specific evaluation from the history later.
Embedding in a Process (businessRuleTask)
Use a businessRuleTask to evaluate a decision inside a BPMN process. There are two ways to reference the decision:
Option A — stateway:decisionRef attribute (concise): set the decision key directly as an attribute on the element. Optionally add stateway:decisionVersion to pin a specific version.
Option B — stateway:TaskHeaders (explicit): use a TaskHeader child element with key="decisionKey".
Both options are accepted; stateway:decisionRef takes precedence when both are present.
<!-- Option A: decisionRef attribute (concise) -->
<bpmn:businessRuleTask id="calcDiscount" name="Calculate Discount"
stateway:decisionRef="discount-calculator">
<bpmn:extensionElements>
<stateway:IoMapping>
<stateway:Input source="{{variables.customerType}}" target="customerType" />
<stateway:Input source="{{variables.orderTotal}}" target="orderTotal" />
<stateway:Output source="{{discountPercent}}" target="discountPercent" />
</stateway:IoMapping>
</bpmn:extensionElements>
</bpmn:businessRuleTask>
<!-- Option B: TaskHeaders (explicit, compatible with other task types) -->
<bpmn:businessRuleTask id="calcDiscount" name="Calculate Discount">
<bpmn:extensionElements>
<stateway:TaskHeaders>
<stateway:TaskHeader key="decisionKey" value="discount-calculator" />
</stateway:TaskHeaders>
<stateway:IoMapping>
<stateway:Input source="{{variables.customerType}}" target="customerType" />
<stateway:Input source="{{variables.orderTotal}}" target="orderTotal" />
<stateway:Output source="{{discountPercent}}" target="discountPercent" />
</stateway:IoMapping>
</bpmn:extensionElements>
</bpmn:businessRuleTask>
target is the process variable name — write it plain (discountPercent), not variables.discountPercent. A dotted target is rejected when you publish the definition.
To pin a specific decision version (useful for audit stability), add stateway:decisionVersion:
<bpmn:businessRuleTask id="calcDiscount" stateway:decisionRef="discount-calculator" stateway:decisionVersion="2">
...
</bpmn:businessRuleTask>
When decisionVersion is absent, the engine uses the currently active version.
DMN evaluation in a businessRuleTask is always synchronous. The executionMode header is not supported for this task type and will cause the instance to error if set to async.
In JSON format:
{
"id": "calc-discount",
"type": "businessRuleTask",
"name": "Calculate Discount",
"taskHeaders": { "decisionKey": "discount-calculator" },
"ioMapping": {
"inputs": [
{ "source": "{{variables.customerType}}", "target": "customerType" },
{ "source": "{{variables.orderTotal}}", "target": "orderTotal" }
],
"outputs": [
{ "source": "{{discountPercent}}", "target": "discountPercent" }
]
},
"outgoing": ["flow-next"]
}
When a businessRuleTask runs inside a process, the evaluation is automatically saved with the instance_id and element_id for audit purposes.
Managing Decisions
List Decisions
Request:
curl https://api.stateway.io/v1/decisions \
-H "X-API-Key: sw_live_your_key"
Response:
{
"data": [
{
"id": "dmd_01j...",
"key": "discount-calculator",
"name": "Discount Calculator",
"version": 2,
"source_type": "json",
"source_hash": "c3d2e1f0...",
"is_active": true,
"created_at": "2026-04-26T12:00:00.000Z",
"updated_at": "2026-05-10T08:00:00.000Z"
}
]
}
Get a Decision
Request:
curl https://api.stateway.io/v1/decisions/discount-calculator \
-H "X-API-Key: sw_live_your_key"
Response:
{
"data": {
"id": "dmd_01j...",
"key": "discount-calculator",
"name": "Discount Calculator",
"version": 2,
"source_type": "json",
"source_hash": "c3d2e1f0...",
"is_active": true,
"created_at": "2026-04-26T12:00:00.000Z",
"updated_at": "2026-05-10T08:00:00.000Z"
}
}
List Versions
Request:
curl https://api.stateway.io/v1/decisions/discount-calculator/versions \
-H "X-API-Key: sw_live_your_key"
Response:
{
"data": [
{
"id": "dmd_01j...",
"version": 1,
"source_hash": "b2e1f3a4...",
"is_active": false,
"created_at": "2026-04-26T12:00:00.000Z"
},
{
"id": "dmd_02j...",
"version": 2,
"source_hash": "c3d2e1f0...",
"is_active": true,
"created_at": "2026-05-10T08:00:00.000Z"
}
]
}
Export as DMN XML
Export a decision definition as standard DMN 1.3 XML:
curl https://api.stateway.io/v1/decisions/loan-approval/xml \
-H "X-API-Key: sw_live_your_key"
To export a specific version:
curl "https://api.stateway.io/v1/decisions/loan-approval/xml?version=1" \
-H "X-API-Key: sw_live_your_key"
| Parameter | Description |
|---|---|
version | Optional. Integer ≥ 1. Defaults to the currently active version. |
Response headers:
Content-Type: application/xml
Content-Disposition: attachment; filename="loan-approval.dmn"
XML export is only available for decisions published with source_type: dmn_xml. Decisions published as json or yaml return 422 PROCESS_ERROR — Stateway does not convert between formats.
Create a New Version
Request:
curl -X PUT https://api.stateway.io/v1/decisions/discount-calculator \
-H "X-API-Key: sw_live_your_key" \
-H "Content-Type: application/json" \
-d '{
"source_type": "json",
"source": {
"key": "discount-calculator",
"name": "Discount Calculator",
"hitPolicy": "FIRST",
"inputs": [
{ "name": "customerType", "type": "string" },
{ "name": "orderTotal", "type": "number" }
],
"outputs": [
{ "name": "discountPercent", "type": "number", "defaultValue": 0 }
],
"rules": [
{ "id": "rule-1",
"conditions": {
"customerType": { "operator": "=", "value": "premium" },
"orderTotal": { "operator": ">=", "value": 1000 }
},
"outputs": { "discountPercent": 20 } },
{ "id": "rule-2",
"conditions": {
"customerType": { "operator": "=", "value": "premium" },
"orderTotal": { "operator": "<", "value": 1000 }
},
"outputs": { "discountPercent": 12 } },
{ "id": "rule-3",
"conditions": {
"customerType": { "operator": "=", "value": "standard" },
"orderTotal": { "operator": ">=", "value": 500 }
},
"outputs": { "discountPercent": 5 } },
{ "id": "rule-4", "conditions": {}, "outputs": { "discountPercent": 0 } }
]
}
}'
Response:
{
"data": {
"id": "dmd_02j...",
"key": "discount-calculator",
"version": 2,
"name": "Discount Calculator",
"source_type": "json",
"source_hash": "c3d2e1f0...",
"is_active": true,
"created_at": "2026-05-10T08:00:00.000Z"
}
}
Delete a Decision
Request:
curl -X DELETE https://api.stateway.io/v1/decisions/discount-calculator \
-H "X-API-Key: sw_live_your_key"
Returns 204 No Content.
Rollback to a Previous Version
Request:
curl -X POST https://api.stateway.io/v1/decisions/discount-calculator/rollback \
-H "X-API-Key: sw_live_your_key" \
-H "Content-Type: application/json" \
-d '{ "version": 1 }'
Response:
{
"data": {
"id": "dmd_01j...",
"key": "discount-calculator",
"version": 1,
"is_active": true,
"updated_at": "2026-05-10T08:00:00.000Z"
}
}
Rollback atomically switches is_active. The next PUT (new version) uses MAX(version) + 1 to avoid collisions. Existing process instances that evaluated this decision are not affected.
Evaluation History
List Evaluations
Request:
curl "https://api.stateway.io/v1/decisions/discount-calculator/evaluations" \
-H "X-API-Key: sw_live_your_key"
Available query filters:
| Parameter | Type | Description |
|---|---|---|
instance_id | UUID | Filter by process instance |
from | ISO 8601 datetime | Evaluations on or after this time |
to | ISO 8601 datetime | Evaluations before or on this time |
version | integer | Filter by decision version |
limit | integer | Results per page (default: 50, max: 100) |
Response:
{
"data": [
{
"evaluation_id": "eval_01j...",
"decision_key": "discount-calculator",
"decision_version": 2,
"decision_hash": "c3d2e1f0...",
"matched": true,
"rules_matched": ["rule-1"],
"outputs": { "discountPercent": 15 },
"instance_id": "inst_01j...",
"element_id": "calc-discount",
"evaluated_at": "2026-05-01T10:30:00.000Z"
}
]
}
Get a Specific Evaluation
Request:
curl https://api.stateway.io/v1/decisions/discount-calculator/evaluations/eval_01j... \
-H "X-API-Key: sw_live_your_key"
Response:
{
"data": {
"evaluation_id": "eval_01j...",
"decision_key": "discount-calculator",
"decision_version": 2,
"decision_hash": "c3d2e1f0...",
"matched": true,
"rules_matched": ["rule-1"],
"outputs": { "discountPercent": 15 },
"instance_id": "inst_01j...",
"element_id": "calc-discount",
"evaluated_at": "2026-05-01T10:30:00.000Z"
}
}
The decision_hash in each evaluation record links it to the exact decision version that was in use at evaluation time — useful for audit trails when the decision has since been updated.
Input/Output Types
| Type | Description | Example |
|---|---|---|
string | Text values | "premium", "active" |
number | Numeric values | 1500, 3.14 |
boolean | True/false | true, false |
An input may also carry an optional description; an output may carry an optional defaultValue
(returned when no rule matches).
Best Practices
- Choose the right hit policy —
FIRSTfor priority-based decisions,ALLfor comprehensive results,UNIQUEfor strict validation - Order rules carefully — with
FIRST, rule order determines precedence - Add an empty-
conditionscatch-all rule last — ensures every input combination produces a result - Evaluate standalone before embedding — verify decision logic via
POST /evaluatebefore wiring it into a process - Use
evaluation_idfor audit — standalone evaluations can be retrieved later for compliance and debugging - Version decisions independently — decision models may change more frequently than process definitions; use separate version cycles