What is a Fact Definition?
A Fact Definition declares an input variable that your rules can reference in conditions and actions. Think of it like a function parameter — you define the name and type before writing the logic. Facts are tenant-wide: once defined, they’re available across all policy groups and versions.Pre-registered Facts
Every tenant starts with these pre-registered facts, available immediately after account creation:userId and userTags are provided for convenience — they are regular facts, not protected. You can rename, retype, or delete them exactly as you would any custom fact, under the same rule (see Managing Facts). They can be used in conditions and actions immediately, with no registration needed.
isBlocked and blockReason are system facts. They hold the values the BLOCK action writes, so the engine fixes their shape:
- You cannot delete them or change their type (
FD-003). - No
SET_FACTorMUTATE_FACTcan target them (ACT-033). OnlyBLOCKwrites them. - You can still rename them, change their description, mark them as PII, and give
blockReasona value domain.
BLOCK writes during the same request (see How a request runs). To use a previous call’s block in a condition, send it under another key such as previouslyBlocked. If you send isBlocked: true and BLOCK writes true again, the value does not change, so it comes back in neither mutatedFacts nor generatedVariables.
Custom Facts
Any input variable beyond the pre-registered facts should be registered before use (see Unregistered Facts). For example,customerTier, orderRegion, or productCategory are custom facts.
Supported Types
Value Domains
A type says a fact holds a number. A value domain says which numbers. Declare one and the platform refuses a rule that could never match, and the rule editor offers a dropdown instead of a free text box.BOOLEAN takes neither — it has only two values to begin with. For list types the value domain
applies to each element, not to the list as a whole.
What changes once you declare one
- Saving a rule. A literal outside the domain is refused, so
tier == "PLATNUM"is caught at save time instead of silently never matching. - Writing a rule. The console turns the value box into a dropdown for scalar types.
- Executing. An input value outside the domain returns
400. Before, sending an out-of-domain value was more permissive than omitting the fact — omitting one the rules read was already a400.
min and max only. A boundary need not be one of
the allowed values: loyaltyPoints > 50 is valid even when the allowed values are
[0, 100, 200], because 50 is a threshold, not a value the fact will hold.
CONTAINS is exempt entirely. tier CONTAINS "OLD" legitimately matches "GOLD", and "OLD"
is not an allowed value.
Changing a domain later
Execution reads the copy taken when the version was published, not today’s definition.- Changing a domain does not touch a version that is already published, including one you deploy or roll back to later. The change reaches execution once you publish a new version and deploy it.
- Changing a domain never rewrites a rule, and it never blocks the update, even when rules use values the new domain does not accept. The update lists those rules instead.
Rules outside a changed domain
When an update changes a fact’s value domain (declares one, narrows it, or replaces it), LexQ checks the rules that compare that fact, using the same check that runs when a rule is saved (P-051). The rules whose conditions use a value the new domain does not accept come back with
the update.
- Which versions. Drafts, and the versions currently deployed: each group’s live version and its A/B test version. Versions that are only published, versions whose effective window has ended, and versions in deleted groups are left out. You cannot edit them, and they keep the copy taken when they were published.
- How much. Every version is listed. Each shows up to five rules in priority order, with every affected condition exactly as the rule has it, and counts the rest. Disabled rules are included.
- When. Only when the update changes the stored domain. Sending the same domain again lists nothing. Removing a domain lists nothing either, because no value is outside it.
Do not just swap in an allowed value. After
BRONZE is removed from tier,
tier NOT_EQUALS "BRONZE" is true for every allowed tier, while tier NOT_EQUALS "SILVER" would
start excluding silver customers.
Where the list appears:
Removing
BRONZE from the allowed values of tier prints the updated fact on stdout and this on
stderr:
API response: meta.valueDomainMismatch
API response: meta.valueDomainMismatch
meta of the update response for the same example. data holds the updated fact.Naming Conventions
A fact key starts with a letter, then letters, numbers, and underscores. Keys are case-sensitive, sopaymentAmount and paymentamount are two different facts.
Casing is yours to choose. LexQ’s own facts and every example in these docs use
camelCase, but the engine accepts any casing, and a key you already created keeps working.
- ✅
paymentAmount,customerTier,userRegion - ✅
payment_amount,PAYMENT_AMOUNT— valid too, just not what these docs show - ❌
Payment-Amount,PAYMENT AMOUNT,1stPurchase,_internal
Required vs Optional Facts
The rules decide which keys a request must send, not the fact definition. Every key that an enabled rule in the executed version reads must be present in the request: its condition fields, and thetargetVar and refVar of MUTATE_FACT. If one is missing, the request fails with P-015.
To leave a value out on purpose, send the key with null. A condition on a null value does not match. A MUTATE_FACT target is the exception: when the action runs, its value must be a number. If it is null at that point, the request fails with P-003.
The Required flag on a fact definition is informational only. It has no effect at execution, and it will be removed.
PII Facts
A fact can be marked as PII (personally identifiable information) in the Console under Management → Fact Definitions, or with the CLI:--pii on lexq facts create or lexq facts update, and --no-pii to clear it. On update, both flags need CLI 1.1.6 or later. Marking changes how the value is read, not how rules run:
- Rules evaluate the real value as usual — masking is aimed at humans and integrations, not at the engine.
- On every read surface (Decision Provenance, execution history detail, exports) the value arrives masked as
••••••, andmaskedKeyslists what was masked. - Revealing a masked value is console-only and audited: the audit row is written before the value is shown. See Decision Provenance for the full contract.
Unregistered Facts
A key that a rule only reads (a condition field, or arefVar) can be referenced before you register it as a Fact Definition. LexQ does not force registration first for those, but whenever you save a rule, it tells you which referenced keys are not in the schema.
An action target is different. The targetVar of SET_FACT and MUTATE_FACT must be a registered fact. Saving or publishing a rule that writes to an unregistered key fails with ACT-032, so register the fact first.
On every rule create or update, LexQ scans the rule’s conditions and actions and returns any unregistered facts — keys that no Fact Definition declares — each with an inferred type, a suggested display name, and where it was referenced (condition or action).
For keys a rule only reads, this is non-blocking by design: saving, publishing, and deploying are not stopped. The rule is stored exactly as written. A key it reads must still be sent at execution, registered or not (see Required vs Optional Facts).
What stays off until you register the fact:
Registering a fact also adds a type check before execution. A request that sends another type for a registered fact the version reads, such as
"85" for a NUMBER, fails with P-060 before any rule is checked. null is not a mismatch. Like the value domain, the check reads the declarations copied when the version was published, so a fact you register after a version is published is not checked in that version. A key that is not registered is never checked: the condition does not match, and that rule’s decisionTraces[].reasonDetail reads Evaluation error: FACT_TYPE_MISMATCH.
The unregistered-fact warning surfaces on every authoring surface: the CLI (stderr), the Console rule editor and deploy dialog, and the MCP tools.
Reviewing and registering
A save returns its unregistered facts inline, but you can also list them for an entire version at any time:Registration is always an explicit step — LexQ never auto-creates a fact from a rule reference. Your schema stays a deliberate contract, not a side effect of rule authoring.
Managing Facts
Facts are managed in the Console under Management → Fact Definitions, or via the CLI:lexq_facts_update work
the same way. Leave out --name or --description and it stays as it is. To remove a
description, pass an empty one:
name fails with C-001.
Next Steps
Policy Rules
Use facts in rule conditions and actions.
Policy Execution
See how facts are passed during execution.

