Skip to main content

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_FACT or MUTATE_FACT can target them (ACT-033). Only BLOCK writes them.
  • You can still rename them, change their description, mark them as PII, and give blockReason a value domain.
A condition can use them, but it reads only the value your request sends. It never sees what 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.
Every field is optional. Leave them all out and the fact behaves exactly as it did before.

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 a 400.
Comparison operators are checked against 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.
Declare the domain when you create the fact. Rules written afterwards get the dropdown, and the typos a domain catches are the ones that are hardest to notice — a rule that saves cleanly and then never fires.

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.
What a listed version needs depends on its kind: 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:
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, so paymentAmount 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 the targetVar 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 ••••••, and maskedKeys lists 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.
Mark facts like emails, phone numbers, and account identifiers as PII at creation time — masking starts the moment the flag is on, with no rule changes needed.

Unregistered Facts

A key that a rule only reads (a condition field, or a refVar) 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:
Then register the ones you intend to keep:
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:
An update changes only the fields you send, and the API and the MCP tool 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:
A display name cannot be removed: an empty or blank name fails with C-001.
Use lexq analytics requirements (CLI) or the Engine API’s GET /groups/{groupId}/requirements endpoint to see exactly which facts a deployed version needs.
A fact that rules reference cannot be deleted, and its type cannot be changed. Both calls return 409 — FD-006 for the delete, FD-007 for the type change — and report how many rules reference it. Remove those references first, or create a new fact.This holds regardless of version status: a draft counts the same as a deployed version. Rules in a deleted group don’t count, because a deleted group can’t be edited or deployed. You can still replay a deleted group after you delete or retype the fact. Renaming a fact and toggling required are never blocked.System facts are the exception: deleting one or changing its type fails with FD-003 whether or not rules reference it.

Next Steps

Policy Rules

Use facts in rule conditions and actions.

Policy Execution

See how facts are passed during execution.