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 — 1 is the highest. Within a version, priorities always form a contiguous 1…N sequence: new rules are appended at the end, and reordering rewrites the whole sequence.
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:

GROUP Condition

A branch node that combines child conditions with AND or OR:

Operators

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.
Do not use CONTAINS on a list fact. CONTAINS is substring matching on a STRING. For LIST_STRING / LIST_NUMBER facts use HAS_ANY / HAS_ALL / HAS_NONE, and send the value as an array (["electronics"]), not a comma-joined string.IN / NOT_IN are the mirror image: a scalar fact checked against a list value. HAS_* are list-on-both-sides.

Value Types

Nested Conditions Example

(customerTier = "VIP" AND paymentAmount >= 100000) OR region IN ["KR", "JP"]
In the example above, customerTier, region, and paymentAmount are custom facts that should be registered in Fact Definitions. You can reference a fact before registering it — LexQ flags any unregistered key with a non-blocking warning rather than rejecting the rule — but registering enables type validation and Required Input Facts analysis. Only userId and userTags 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. The engine never calls external systems. It mutates facts and records decisions; your caller reads the response and acts on it. This preserves audit-grade separation between engine state and external state.

MUTATE_FACT parameters

operator × method matrix DIV + AMOUNT requires operand ≠ 0.
targetVar must be present in the request facts as a number. A missing fact throws — the engine never defaults to 0. Accumulation scenarios must send a starting value (pointsEarned: 0); the engine is stateless and does not read your database.Creating a fact from nothing is SET_FACT’s job.

When targetVar and refVar differ

refVar is only meaningful in PERCENTAGE × . Specifying it in any other cell is an error, not a silent no-op — AMOUNT has no base concept, and MUL × PERCENTAGE is a multiplier shorthand that reads no base. Use it when the base differs from the target:
loyaltyPoint += orderTotal × 5% — two different facts. Omitting refVar would compute loyaltyPoint += loyaltyPoint × 5% instead.
Ranges are not constrained. Negative operands and percentages above 100 are valid — refunds (-5), surcharges (150), risk scores, and game points all need them. Range validation belongs in your fact definitions, not the engine.

MUTATE_FACT — Percentage

Reduce paymentAmount by 10%. Currency facts should also set rounding — see below.

MUTATE_FACT — Fixed Amount

MUTATE_FACT — With Rounding

Without rounding, the result is preserved at full precision. Specify it when you need currency-scale output.
mode accepts UP, DOWN, CEILING, FLOOR, HALF_UP, HALF_DOWN, HALF_EVEN. Defaults to HALF_UP.

SET_FACT — Assign a value

Assigns a literal of any type. This is the only action that creates a fact that did not exist.
SET_FACT assigns rather than appends. To accumulate into a LIST_STRING fact, send the current list in the request and assign the full new list. Because it is an assignment and not an arithmetic change, SET_FACT produces no __delta.

BLOCK — Record a rejection

BLOCK does not halt execution. Subsequent actions in the same rule and subsequent winning rules still run. It writes the isBlocked fact; enforcement is your caller’s responsibility.

Generated Variables

In addition to the mutated facts, the engine exposes per-action change information in generatedVariables: 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. 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

DecisionReasonCode

Status × ReasonCode Mapping

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.
CONDITION_MISMATCH covers two different facts: the condition evaluated to false, and the condition could not be evaluated (type mismatch, unresolved fact). The specific cause is in reasonDetail — for example Evaluation error: FACT_TYPE_MISMATCH when a LIST_STRING fact receives a plain string. A rule that fails to evaluate does not stop the execution; the rest of the rules run normally.

Complete Rule Example

Next Steps

Fact Definitions

Define the input variables your rules expect.

Dry Run

Test your rules before publishing.