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 contiguous1…N sequence: new rules are appended at the end, and reordering rewrites the whole sequence.
Condition Syntax
Conditions use a tree structure with two node types:SINGLE and GROUP.
SINGLE Condition
A leaf node that compares a single fact against a value:GROUP Condition
A branch node that combines child conditions withAND 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.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. A key that a rule only reads can be referenced before you register it — LexQ flags any unregistered key with a non-blocking warning rather than rejecting the rule — but registering enables type validation and value domains. An action target must be registered first (see Action Types). Some facts are pre-registered for every tenant (see Pre-registered Facts).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.
The
targetVar of SET_FACT and MUTATE_FACT must be a registered fact. Saving or publishing a rule whose action writes to an unregistered key fails with ACT-032, so register it in Fact Definitions first.
Only BLOCK writes isBlocked and blockReason. Saving or publishing a rule whose SET_FACT or MUTATE_FACT targets either key fails with ACT-033, even though both are registered.
MUTATE_FACT parameters
operator × method matrix
DIV + AMOUNT requires operand ≠ 0.
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 — declare a
value domain with min and max on the fact itself.That constrains the input a caller sends, not what an action computes. A MUTATE_FACT may
still take paymentAmount outside the declared range; that outcome belongs to a
Dry Run.MUTATE_FACT — Percentage
ReducepaymentAmount by 10%. Currency facts should also set rounding — see below.
MUTATE_FACT — Fixed Amount
MUTATE_FACT — With Rounding
Withoutrounding, 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 can write a key the request did not send. ThetargetVar still has to be a registered fact.
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. If a MUTATE_FACT also changes the same fact, its __delta includes the SET_FACT change (see How a request runs).
BLOCK — Record a rejection
How a request runs
A request runs in two steps:- Every rule’s condition is checked against the facts in the request. This happens before any action runs, so a condition never sees a value that an action writes during the same request.
- The actions of the matching rules run in priority order. Each action works on the values left by the actions before it, including actions in earlier rules.
customerTier: "STAFF" and paymentAmount: 100, and two rules match:
Rule 2 still matches, because its condition sees the request value 100, not 20. Its action multiplies the 20 that rule 1 left. The response has
mutatedFacts.paymentAmount = 60 and generatedVariables.paymentAmount__delta = -40. The delta compares the final value with the request value, so it includes the SET_FACT change.
If both rules are in the same EXCLUSIVE mutex group, only rule 1 fires and the result is 20.
When a later decision has to depend on an earlier result, call the API again with that result, or put both actions in one rule.
Generated Variables
In addition to the mutated facts, the engine exposes per-action change information ingeneratedVariables:
When several actions change the same fact, in one rule or across rules,
__delta reports the total change from the request value. 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 categoryreasonCode— 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
paymentAmount and discountApplied, as facts before you save this rule.
Next Steps
Fact Definitions
Define the input variables your rules expect.
Dry Run
Test your rules before publishing.

