# Rule

> A visibility or pricing rule, authored on a drag-and-drop surface and evaluated by the engine.

Source: CBX documentation, version 3.x (released). Canonical page: https://docs.configbox.at/docs/technical/property-types/rule. Last updated 2026-07-27.

---
A visibility or pricing rule, authored on a drag-and-drop surface and evaluated by the engine.

**Storage kind:** `column` · **Column:** `text`

Use it wherever the engine expects a rule — a question's or answer's visibility condition, a
shipping or discount rule. It is not a general-purpose structured-data type: the stored JSON is the
rule engine's own vocabulary, and only the rule engine reads it. For an arbitrary JSON document use
[`json`](https://docs.configbox.at/docs/technical/property-types/json).

## Settings

| Setting | Meaning |
|---|---|
| `textWhenNoRule` | What the form shows when no rule is set |

Everything else is the shared settings. The editing surface, the condition types and the vocabulary
come from the rule engine, not from the property definition.

## Validation is not optional here

The engine turns the stored JSON into a PHP boolean expression and `eval()`s it
(`ConfigboxRulesHelper::getConditionsCode`). The failure mode of a rule that does not hold together
is therefore **not** "behaves oddly":

| Malformed how | What happens at runtime |
|---|---|
| Conditions not alternating with AND/OR | a `ParseError` |
| An unknown condition type | an exception in every list that renders the rule |
| A reference to a deleted question, answer or calculation | a rule that **quietly hides** whatever it governs |

That last row is the dangerous one — it is silent. `check()` refuses malformed rules on the way in,
and because it is the standard Kenedo validation hook, one gate covers both write paths: the admin
form and the MCP `storeData`, since both go through `KenedoModel::validateData()`.

Every complaint about one rule travels as a single refusal carrying all of them, so the author fixes
the whole thing at once rather than one condition per round trip.

## Only a changed rule is judged

`check()` first asks whether this save actually changes the rule, and skips validation if not.

That is load-bearing. `KenedoController::store()` and the MCP write path both seed the request from
the existing record, so an **untouched rule is posted back verbatim on every partial update**.
Without the check, a rule written by an older editor — or against an earlier vocabulary — would
block edits to fields that have nothing to do with it: you could not fix a typo in a question's
title because its rule had gone stale.

The comparison reads the column directly rather than through `getRecord()`, whose memoized cache
differs between the web process and the CLI. An insert, or any value that cannot be read back, is
treated as changed and validated.

## Notes

- It publishes a **structured sub-schema** to MCP rather than being described as free text, so an
  assistant authors a rule structurally instead of hand-building the engine's infix token stream.
  `ConfigboxMcpHelper` special-cases `'rule'` and substitutes
  `ConfigboxMcpRulesHelper::getVocabularySchema()` for the field (`helpers/mcp.php`). That is the
  exception to the rule that a `text` column is opaque.
- `copyRule()` runs during a record copy, re-pointing references to the copied questions and answers
  rather than leaving the copy pointing at the original's — the same shape as `calculation`'s
  `copyCalculation()`. It honours `storeExternally`.
- `text` is 64 KB. A very large rule needs an explicit wider `dataType`.

@see json.md — the counter-example, where no structure is imposed
@see ../com_configbox_rule_engine.md
@see ../com_configbox_mcp_server.md — the sub-schema an assistant authors against
@see ../../admin-guide/rules/build-a-rule.md — what the operator sees
