Skip to main content

What is a Policy Rule?

A Policy Rule is a condition → actions pair. When the input facts satisfy the condition, the rule’s actions fire. Rules are evaluated in priority order (0 = highest priority).
IF condition matches → THEN execute actions
Each rule belongs to a specific policy version and has a name, a priority, a condition (tree-structured logic), and one or more actions.

Condition Syntax

Conditions use a tree structure with two node types: SINGLE and GROUP.
LexQ uses its own condition DTO format with type: "SINGLE" / type: "GROUP", field, operator, value, and valueType fields. Do not confuse this with the Console’s internal form representation which may use different field names. Always use the engine format when calling the API or CLI.

SINGLE Condition

A leaf node that compares a single fact against a value:
{
  "type": "SINGLE",
  "field": "payment_amount",
  "operator": "GREATER_THAN_OR_EQUAL",
  "value": 100000,
  "valueType": "NUMBER"
}

GROUP Condition

A branch node that combines child conditions with AND or OR:
{
  "type": "GROUP",
  "operator": "AND",
  "children": [
    { "type": "SINGLE", "field": "customer_tier", "operator": "EQUALS", "value": "VIP", "valueType": "STRING" },
    { "type": "SINGLE", "field": "payment_amount", "operator": "GREATER_THAN", "value": 50000, "valueType": "NUMBER" }
  ]
}

Operators

OperatorCompatible TypesDescription
EQUALSAllExact match
NOT_EQUALSAllNegation
GREATER_THANNUMBER>
GREATER_THAN_OR_EQUALNUMBER>=
LESS_THANNUMBER<
LESS_THAN_OR_EQUALNUMBER<=
CONTAINSSTRINGSubstring match
INSTRING, NUMBERValue is in the provided list
NOT_INSTRING, NUMBERValue is not in the provided list
For IN / NOT_IN, the Compatible Types column refers to the type of the fact being checked (STRING or NUMBER). The value you provide is the list itself — its valueType is LIST_STRING or LIST_NUMBER.

Value Types

TypeJSON ValueExample
STRING"string""VIP"
NUMBERnumber100000
BOOLEANtrue / falsetrue
LIST_STRING["a", "b"]["KR", "US"]
LIST_NUMBER[1, 2][10000, 20000]

Nested Conditions Example

(customer_tier = "VIP" AND payment_amount >= 100000) OR region IN ["KR", "JP"]
{
  "type": "GROUP",
  "operator": "OR",
  "children": [
    {
      "type": "GROUP",
      "operator": "AND",
      "children": [
        { "type": "SINGLE", "field": "customer_tier", "operator": "EQUALS", "value": "VIP", "valueType": "STRING" },
        { "type": "SINGLE", "field": "payment_amount", "operator": "GREATER_THAN_OR_EQUAL", "value": 100000, "valueType": "NUMBER" }
      ]
    },
    { "type": "SINGLE", "field": "region", "operator": "IN", "value": ["KR", "JP"], "valueType": "LIST_STRING" }
  ]
}
In the example above, customer_tier, region, and payment_amount are custom facts that must be registered in Fact Definitions before use. Only user_id and user_tags are system facts available by default.

Action Types

LexQ engine actions are domain-agnostic primitives. The engine sees only numbers and structures — it does not assume commerce, fintech, insurance, or any specific business model. Domain-specific semantics live in your fact names and integration payloads.
TypeDescriptionKey Parameters
MUTATE_FACTMutate a numeric fact in place using an arithmetic operatorrefVar, operator, method, rate (when PERCENTAGE) or value, rounding (optional)
INCREMENT_FACTIncrement a numeric fact by a calculated amounttargetVar, method, refVar,rate (when PERCENTAGE) or value, rounding (optional)
EMIT_EVENTEmit an event to an external integrationintegrationId, eventPayload
EMIT_NOTIFICATIONSend a notification through an integrationintegrationId, targetVar, notificationPayload
EMIT_WEBHOOKCall an external URL or webhook integrationurl, method, payloadTemplate (optional)
BLOCKBlock the requestreason
SET_FACTSet a fact to a fixed valuekey, value
ADD_TAGAppend a tag to a list facttag, targetVar

MUTATE_FACT — Percentage

Reduce payment_amount by 10%:
{
  "type": "MUTATE_FACT",
  "parameters": {
    "refVar": "payment_amount",
    "operator": "SUB",
    "method": "PERCENTAGE",
    "rate": 10
  }
}
If payment_amount is 200,000 → reduced to 180,000. The change (-20,000) is exposed as payment_amount__delta in generatedVariables.

Operators

OperatorAMOUNTPERCENTAGE
ASSIGNrefVar = valuerefVar = refVar × rate / 100
ADDrefVar += valuerefVar += refVar × rate / 100
SUBrefVar -= valuerefVar -= refVar × rate / 100
MULrefVar *= valuerefVar *= (rate / 100 + 1)
DIVrefVar /= valueinvalid — use MUL with the inverse rate
DIV + PERCENTAGE is rejected at draft save time. DIV + value=0 is also rejected.

MUTATE_FACT — Fixed Amount

{
  "type": "MUTATE_FACT",
  "parameters": {
    "refVar": "payment_amount",
    "operator": "SUB",
    "method": "AMOUNT",
    "value": 5000
  }
}

MUTATE_FACT — With Rounding

By default, calculator output is preserved at full precision (lossless). Use the optional rounding field when you need a fixed scale.
{
  "type": "MUTATE_FACT",
  "parameters": {
    "refVar": "payment_amount",
    "operator": "SUB",
    "method": "PERCENTAGE",
    "rate": 13.7,
    "rounding": { "scale": 0, "mode": "FLOOR" }
  }
}
rounding.scale is an integer in [0, 16]. rounding.mode is one of HALF_UP (default), HALF_DOWN, HALF_EVEN, FLOOR, CEILING, DOWN, UP. Omitting rounding keeps full precision — round downstream (e.g. when rendering a currency display) instead of in the engine.

INCREMENT_FACT — Percentage

Add 1% of payment_amount to total_point:
{
  "type": "INCREMENT_FACT",
  "parameters": {
    "targetVar": "total_point",
    "refVar": "payment_amount",
    "method": "PERCENTAGE",
    "rate": 1
  }
}
If payment_amount is 100,000 → total_point is incremented by 1,000. The increment is exposed as total_point__delta in generatedVariables.
INCREMENT_FACT only mutates engine-side facts. To synchronize with an external system (e.g. a points service), compose [INCREMENT_FACT, EMIT_EVENT] as a chain in the same rule. The engine does not call external systems on behalf of INCREMENT_FACT — this preserves audit-grade separation between engine state and external state.

INCREMENT_FACT — Fixed Amount

{
  "type": "INCREMENT_FACT",
  "parameters": {
    "targetVar": "total_point",
    "method": "AMOUNT",
    "value": 500
  }
}

EMIT_EVENT

Emit a generic event payload to an external integration. The engine validates only that integrationId and a non-empty eventPayload map are present — payload structure is the integration provider’s responsibility.
{
  "type": "EMIT_EVENT",
  "parameters": {
    "integrationId": "<your-integration-id>",
    "eventPayload": {
      "couponId": "WELCOME_VIP_2026"
    }
  }
}
Domain-specific keys (couponId, ticketId, claimId, etc.) are routed by the integration provider. This makes EMIT_EVENT a true generic primitive — usable for coupon issuance, ticket creation, insurance claim submission, or any event-style external call.

EMIT_NOTIFICATION

Send a notification through an external integration.
{
  "type": "EMIT_NOTIFICATION",
  "parameters": {
    "integrationId": "<your-notification-integration-id>",
    "targetVar": "phone_number",
    "notificationPayload": {
      "channel": "SMS",
      "templateId": "ORDER_CONFIRM_001",
      "variables": { "order_id": "order_id" }
    }
  }
}
targetVar identifies the recipient fact (e.g. phone_number, email, device_token). notificationPayload carries channel, template, and variable mapping for the integration provider — the engine does not interpret these keys.

BLOCK — Block the Request

{
  "type": "BLOCK",
  "parameters": { "reason": "Suspected fraud" }
}

SET_FACT — Output Variable

SET_FACT is a literal value setter, not an expression evaluator. It assigns a fixed value to a key in the output. It does not support arithmetic expressions or references to other facts.
{
  "type": "SET_FACT",
  "parameters": { "key": "risk_level", "value": "HIGH" }
}

ADD_TAG

{
  "type": "ADD_TAG",
  "parameters": {
    "tag": "VIP_VERIFIED",
    "targetVar": "user_tags"
  }
}
Appends the tag to the list fact. Duplicates are automatically prevented. If targetVar is omitted, defaults to user_tags.

EMIT_WEBHOOK — Basic

{
  "type": "EMIT_WEBHOOK",
  "parameters": {
    "url": "https://api.example.com/webhooks/orders",
    "method": "POST"
  }
}
Without payloadTemplate, the engine sends all input/output facts as the request body (system variables excluded).

EMIT_WEBHOOK — With Payload Template

Use payloadTemplate to customize the request body. Template variables are resolved at execution time.
{
  "type": "EMIT_WEBHOOK",
  "parameters": {
    "url": "<integration-id-or-direct-url>",
    "method": "POST",
    "payloadTemplate": {
      "text": "🔔 Rule {{ruleName}} fired\nCustomer: {{fact.customer_tier}}\nAmount: {{output.payment_amount}}"
    }
  }
}

Available Template Variables

VariableDescriptionExample Value
{{fact.xxx}}Input fact value{{fact.payment_amount}}150000
{{output.xxx}}Output variable (after actions){{output.payment_amount__delta}}-20000
{{timestamp}}Execution time (ISO-8601)2026-04-14T05:30:00Z
{{ruleName}}Matched rule nameVIP 20% Discount
{{groupName}}Policy group namePayment Policy
{{versionNo}}Executed version number5
{{xxx}}Direct fact lookup (shorthand){{customer_tier}}VIP
Slack requires {"text": "..."}, Discord requires {"content": "..."}. Use payloadTemplate to match each platform’s expected format.
{{fact.xxx}} and {{output.xxx}} reference the same data map. If an earlier action (e.g., MUTATE_FACT) modifies payment_amount, both {{fact.payment_amount}} and {{output.payment_amount}} reflect the modified value.

Generated Variables

In addition to the mutated facts, the engine exposes per-action change information in generatedVariables:
PatternMeaningGenerated By
{refVar}__deltaNet change to refVar (mutated - original, signed)MUTATE_FACT
{targetVar}__deltaNet increment to targetVar (always non-negative)INCREMENT_FACT
When multiple actions in a rule mutate the same fact, __delta reports the cumulative change. Per-action snapshots are available in executionTraces for audit drill-down.

Mutex — Rule-Level Conflict Resolution

Within a single version, rules can belong to a mutex group to control how many matching rules fire.
FieldDescription
mutexGroupA string key grouping related rules (e.g., "best-discount")
mutexModeNONE (default), EXCLUSIVE, or MAX_N
mutexStrategyFIRST_MATCH, HIGHEST_PRIORITY, or MAX_BENEFIT
mutexLimitMax number of rules to fire from this group (for MAX_N)
When mutexMode is EXCLUSIVE, only the single winning rule fires. When it is MAX_N, up to mutexLimit rules fire in priority order. Other matching rules in the same mutex group are skipped and logged in the Decision Trace with status BLOCKED and reason MUTEX_PRIORITY_LOST or MUTEX_LIMIT_REACHED (see Decision Trace).

Decision Trace

Every rule evaluation produces a decision trace entry explaining whether the rule fired and why. This is the core of LexQ’s audit-grade reasoning — every outcome can be traced back to a specific status and reason code. A decision trace entry has two classifying fields:
  • status — the high-level outcome category
  • reasonCode — the specific reason within that category

DecisionStatus

StatusMeaning
SELECTEDThe rule fired and its actions ran
NO_MATCHThe rule’s condition evaluated to false
NOT_SELECTEDPre-competition filter (e.g., effective date — reserved for future)
BLOCKEDThe rule lost mutex or activation group competition
ERRORAn action or engine error occurred during evaluation

DecisionReasonCode

ReasonCodeMeaning
FINAL_WINNERThe selected rule for this evaluation
EFFECTIVE_DATE_INVALIDOutside the rule’s effective [from, to] window (reserved)
CONDITION_MISMATCHThe rule’s condition tree did not match the input facts
MUTEX_PRIORITY_LOSTEXCLUSIVE mutex (limit=1) — another rule had higher priority or score
MUTEX_LIMIT_REACHEDMAX_N mutex (limit>1) — mutexLimit was filled by higher-priority rules
GROUP_PRIORITY_LOSTEXCLUSIVE activation group (limit=1) — another rule won by priority
GROUP_LIMIT_REACHEDMAX_N activation group (limit>1) — executionLimit was filled
ACTION_ERRORAn action executor threw during this rule’s execution
ENGINE_ERRORThe engine itself failed before/during this rule

Status × ReasonCode Mapping

StatusPossible ReasonCodes
SELECTEDFINAL_WINNER
NO_MATCHCONDITION_MISMATCH
NOT_SELECTEDEFFECTIVE_DATE_INVALID (reserved)
BLOCKEDMUTEX_PRIORITY_LOST, MUTEX_LIMIT_REACHED, GROUP_PRIORITY_LOST, GROUP_LIMIT_REACHED
ERRORACTION_ERROR, ENGINE_ERROR
When debugging “why didn’t my rule fire?”, the decision trace gives you the answer in two steps: the status tells you which category of filtering removed the rule, and the reasonCode tells you exactly which check rejected it.

Complete Rule Example

{
  "name": "VIP 20% Discount",
  "priority": 0,
  "condition": {
    "type": "GROUP",
    "operator": "AND",
    "children": [
      { "type": "SINGLE", "field": "customer_tier", "operator": "EQUALS", "value": "VIP", "valueType": "STRING" },
      { "type": "SINGLE", "field": "payment_amount", "operator": "GREATER_THAN_OR_EQUAL", "value": 100000, "valueType": "NUMBER" }
    ]
  },
  "actions": [
    { "type": "MUTATE_FACT", "parameters": { "refVar": "payment_amount", "operator": "SUB", "method": "PERCENTAGE", "rate": 20 } },
    { "type": "SET_FACT", "parameters": { "key": "discount_applied", "value": true } }
  ],
  "mutexGroup": "best-discount",
  "mutexMode": "EXCLUSIVE",
  "mutexStrategy": "HIGHEST_PRIORITY",
  "isEnabled": true
}

Next Steps

Fact Definitions

Define the input variables your rules expect.

Dry Run

Test your rules before publishing.