Skip to main content

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​

PolicyResult shape
FIRSTOutput object of the first matching rule (rule order matters)
UNIQUEOutput 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_ORDERPlanned — currently behaves like ALL ({ "results": […] }); output-value ordering is not applied yet
PRIORITYPlanned — 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.

OperatorMeaningvalue
=equalsscalar
!=not equalscalar
> >= < <=numeric comparisonnumber
betweenvalue ≤ input ≤ value2 (inclusive)requires both value and value2
ininput is one of the listed valuesarray
not_ininput is none of the listed valuesarray
containsString(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.

note

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"
ParameterDescription
versionOptional. Integer ≥ 1. Defaults to the currently active version.

Response headers:

Content-Type: application/xml
Content-Disposition: attachment; filename="loan-approval.dmn"
warning

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:

ParameterTypeDescription
instance_idUUIDFilter by process instance
fromISO 8601 datetimeEvaluations on or after this time
toISO 8601 datetimeEvaluations before or on this time
versionintegerFilter by decision version
limitintegerResults 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​

TypeDescriptionExample
stringText values"premium", "active"
numberNumeric values1500, 3.14
booleanTrue/falsetrue, 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 — FIRST for priority-based decisions, ALL for comprehensive results, UNIQUE for strict validation
  • Order rules carefully — with FIRST, rule order determines precedence
  • Add an empty-conditions catch-all rule last — ensures every input combination produces a result
  • Evaluate standalone before embedding — verify decision logic via POST /evaluate before wiring it into a process
  • Use evaluation_id for 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