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, 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

PolicyDescription
FIRSTReturns the output of the first matching rule (rule order matters)
ALLReturns outputs of all matching rules as an array
UNIQUEReturns the output if exactly one rule matches; error if multiple match
RULE_ORDERReturns all matching rule outputs in rule order
COLLECTAggregates all matching outputs (sum, count, min, max)
OUTPUT_ORDERReturns matching outputs sorted by output values
PRIORITYReturns 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.

SyntaxMeaning
"> 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.

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": "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:

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

Best Practices

  • Choose the right hit policyFIRST for priority-based decisions, ALL for comprehensive results, UNIQUE for 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 /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