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, 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",
"hitPolicy": "FIRST",
"inputs": [
{ "id": "customerType", "label": "Customer Type", "type": "string" },
{ "id": "orderTotal", "label": "Order Total", "type": "number" }
],
"outputs": [
{ "id": "discountPercent", "label": "Discount %", "type": "number" }
],
"rules": [
{ "id": "rule-1", "conditions": ["premium", ">= 1000"], "outputs": { "discountPercent": 15 } },
{ "id": "rule-2", "conditions": ["premium", "< 1000"], "outputs": { "discountPercent": 10 } },
{ "id": "rule-3", "conditions": ["standard", ">= 500"], "outputs": { "discountPercent": 5 } },
{ "id": "rule-4", "conditions": ["-", "-"], "outputs": { "discountPercent": 0 } }
]
}
}'
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\nhitPolicy: FIRST\ninputs:\n - id: customerType\n label: Customer Type\n type: string\n - id: orderTotal\n label: Order Total\n type: number\noutputs:\n - id: discountPercent\n label: Discount %\n type: number\nrules:\n - id: rule-1\n conditions: [premium, \">= 1000\"]\n outputs: {discountPercent: 15}\n - id: rule-4\n conditions: [\"-\", \"-\"]\n outputs: {discountPercent: 0}\n"
}'
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 | Description |
|---|---|
| FIRST | Returns the output of the first matching rule (rule order matters) |
| ALL | Returns outputs of all matching rules as an array |
| UNIQUE | Returns the output if exactly one rule matches; error if multiple match |
| RULE_ORDER | Returns all matching rule outputs in rule order |
| COLLECT | Aggregates all matching outputs (sum, count, min, max) |
| OUTPUT_ORDER | Returns matching outputs sorted by output values |
| PRIORITY | Returns the output with the highest priority (based on output ordering) |
Example: ALL Hit Policy
With hitPolicy: ALL, both rules below match for orderTotal: 1500:
{
"rules": [
{ "id": "r1", "conditions": ["> 1000"], "outputs": { "tier": "high" } },
{ "id": "r2", "conditions": ["> 500"], "outputs": { "tier": "medium" } }
]
}
The result is an array: [{ "tier": "high" }, { "tier": "medium" }].
FEEL Expression Conditions
Rule conditions use a subset of FEEL (Friendly Enough Expression Language). Each element in conditions corresponds to one input by position.
| Syntax | Meaning |
|---|---|
"> 1000" | input > 1000 |
"<= 500" | input ≤ 500 |
"== 'premium'" | input equals 'premium' |
"[500..1000]" | 500 ≤ input ≤ 1000 (inclusive range) |
"(500..1000)" | 500 < input < 1000 (exclusive range) |
"'gold','silver'" | input is one of the listed values |
"NOT 'blocked'" | input ≠ 'blocked' |
"-" | always matches (wildcard) |
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="variables.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="variables.discountPercent" />
</stateway:IoMapping>
</bpmn:extensionElements>
</bpmn:businessRuleTask>
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": "variables.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"
}
]
}
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",
"hitPolicy": "FIRST",
"inputs": [
{ "id": "customerType", "label": "Customer Type", "type": "string" },
{ "id": "orderTotal", "label": "Order Total", "type": "number" }
],
"outputs": [
{ "id": "discountPercent", "label": "Discount %", "type": "number" }
],
"rules": [
{ "id": "rule-1", "conditions": ["premium", ">= 1000"], "outputs": { "discountPercent": 20 } },
{ "id": "rule-2", "conditions": ["premium", "< 1000"], "outputs": { "discountPercent": 12 } },
{ "id": "rule-3", "conditions": ["standard", ">= 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 |
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 - Use wildcards (
-) as catch-all rules — 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