> For the complete documentation index, see [llms.txt](https://docs.agilecase.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.agilecase.com/developer-documentation/custom-form-scripting/rule-reference.md).

# Rule Reference

The declarative rulesets behind ExecuteAllRules — actions, test types and the order they are applied in.

Most of what a JavaScript label is asked to do comes down to the same handful of decisions: show this when that is set, disable this until this is filled in, complain when these two disagree. Writing each one by hand works, but a form with twenty dependencies becomes twenty blocks of jQuery that nobody wants to touch.

The rule engine is the alternative. A rule is a plain object saying which field to watch, what to test it for, and what to do to which fields when the test passes. A ruleset is an array of them, and `ExecuteAllRules` applies the lot.

```javascript
ThisJSLabel().HideRow();

var showNotesWhenContactMade = [{
    "action": "hide",
    "trigger_id": 6108,
    "target_ids": [6109],
    "test_type": "NOT_EQUALS",
    "test_values": ["Contact Made"],
    "description": "Show call notes only when the outcome is Contact Made"
}];

ExecuteAllRules(showNotesWhenContactMade);
```

Because the label re-runs on every change to the form, so does the ruleset. Rules describe how the form should look for the current values, not what should happen when something changes, and every action has an opposite that is applied when the test fails. That is what makes the ruleset a complete description of the form rather than a list of events.

## The shape of a rule

<table><thead><tr><th width="180">Property</th><th>Required</th><th>What it is</th></tr></thead><tbody><tr><td><code>action</code></td><td>Yes</td><td>What to do to the targets. See <a href="#actions">Actions</a>.</td></tr><tr><td><code>trigger_id</code></td><td>Yes</td><td>The field whose value is tested. May be <code>null</code> for a test that ignores it.</td></tr><tr><td><code>target_ids</code></td><td>Yes</td><td>An array of the field IDs the action applies to. At least one entry, since the rule body runs once per target.</td></tr><tr><td><code>test_type</code></td><td>Yes</td><td>A test name from the table below, or your own function.</td></tr><tr><td><code>test_values</code></td><td>Usually</td><td>An array of values to test against. What is read from it depends on the test.</td></tr><tr><td><code>messages</code></td><td>For some actions</td><td>The text used by <code>output_messages</code> and <code>disable_save</code>.</td></tr><tr><td><code>set_values</code></td><td>For <code>set_values</code></td><td>An array of values, paired with <code>target_ids</code> by position.</td></tr><tr><td><code>description</code></td><td>No</td><td>A note to whoever reads the script next. Ignored at runtime, and worth writing anyway.</td></tr></tbody></table>

{% hint style="info" %}
The rule body runs once per entry in `target_ids`, so every rule needs at least one — including `disable_save`, which acts on the Save button rather than on fields. Give that one exactly one target ID, conventionally the trigger's own: the count is what decides how many times the rule runs, and so how many times its message appears.
{% endhint %}

## Actions

### hide

Hides the target fields' rows when the test passes, and shows them when it does not.

```javascript
{
    "action": "hide",
    "trigger_id": 6502,
    "target_ids": [6505, 6506],
    "test_type": "NOT_EQUALS",
    "test_values": ["Yes"]
}
```

Note the shape of that test. The rule hides when it passes, so "show these when the flag is Yes" is expressed as "hide these when the flag is not Yes". Nearly every layout rule reads this way round, and the `description` is the place to record which way you meant it.

### hide\_and\_blank

Hides the targets as `hide` does, and clears their values when the test passes.

Use it wherever a hidden field's old value would otherwise be saved. A user who ticks "third party involved", fills in the insurer, then unticks it leaves the insurer details sitting in the form, invisible and still submitted. `hide_and_blank` is what stops that.

### hide\_and\_blank\_mandatory

`hide_and_blank`, plus the target's mandatory state follows its visibility: hidden fields become optional, visible ones become required. This is how a field is made mandatory only in the circumstances where it applies.

The mandatory state here is the browser's, applied to the field on screen. It is not the field's configured mandatory setting, and it does not survive into anything else that writes to the field.

### disable

Disables the target inputs when the test passes, leaving their values visible and unchangeable.

A date picker's calendar button is hidden alongside its input, so a disabled date field does not offer a way around itself.

### set\_values

Writes a value into each target. `set_values` is paired with `target_ids` by position: the first value goes to the first target, and so on.

```javascript
{
    "action": "set_values",
    "trigger_id": 6217,
    "target_ids": [6218],
    "test_type": "EQUALS",
    "test_values": ["Year 11"],
    "set_values": ['"GCSE"']
}
```

This is the rules engine's syntax for writing a value, and the counterpart to [`CustomField(id).Value3(value)`](/developer-documentation/custom-form-scripting/scripting-helper-api-reference.md#writing-values) in ordinary JavaScript. Two things about it shape when you reach for which.

#### It takes the value in its stored form

A custom field is a visible control plus a hidden element carrying the value that is submitted. `set_values` writes the string you give it into both, so the form to pass is the **stored** one, which depends on the target's type:

<table><thead><tr><th width="230">Target field type</th><th width="220">Pass</th><th>Notes</th></tr></thead><tbody><tr><td>Date picker</td><td>A moment.js date</td><td>Handled for you — the control and the stored value are both set from it.</td></tr><tr><td>Text box, text area, currency, numeric</td><td><code>'"Approved"'</code></td><td>Quoted, as text is stored. The quotes are part of that form and appear in the box, which suits a field the user does not read.</td></tr><tr><td>Dropdown</td><td><code>'["Approved"]'</code></td><td>The list form, as a selection is stored. <code>'["Please Select"]'</code> is the idiom for clearing one.</td></tr><tr><td>Radio list, checkbox list</td><td><code>'["Approved"]'</code></td><td>The list form. The stored value is what a save and a reload pick up.</td></tr><tr><td>Check box</td><td>Set it through the control</td><td>A check box is ticked rather than filled in — see <a href="/developer-documentation/custom-form-scripting/scripting-helper-api-reference.md#writing-values">Writing values</a>.</td></tr></tbody></table>

That is why existing rulesets are written as `"set_values": ['"' + value + '"']`, and against a dropdown as `"set_values": ["[\"Please Select\"]"]`.

Which of the two to reach for follows from the table. Dates, and values the user does not read — a code, a flag, a field feeding a template — are natural `set_values` targets, and keeping the write in the ruleset puts it alongside the hiding and disabling that go with it. For a value the user sees on screen, and for any list or check box you want ticked as well as stored, work it out in JavaScript and write it with `Value3()`, letting rules handle everything around it. Most non-trivial labels use both.

#### It keeps its targets in step with the test

{% hint style="info" %}
A `set_values` rule sets its targets when the test passes and clears them when it does not, so the field follows the condition in both directions without a second rule to undo it.

That makes it the tool for a value the rule owns outright. For a suggested value the user is then free to adjust, write it with `Value3()` behind your own guard instead — see [Writing a Value With Value3](/developer-documentation/custom-form-scripting/examples/writing-a-value-with-value3.md).
{% endhint %}

### output\_messages

Puts a red validation message under each target when the test passes, and removes it when it does not.

```javascript
{
    "action": "output_messages",
    "trigger_id": 6620,
    "target_ids": [6620],
    "test_type": "BEFORE",
    "test_values": ["01/01/2026"],
    "messages": ["This date is before the start of the retention period."]
}
```

Messages are paired with targets by position, and the last message is reused for any target beyond the end of the array. One message and five targets therefore puts the same text under all five.

The message is displayed, not enforced. It does not stop the form being saved. To do that, add a `disable_save` rule alongside it.

### disable\_save

Disables the Save button on the form the label belongs to, and shows a message beside it.

```javascript
{
    "action": "disable_save",
    "trigger_id": 6426,
    "target_ids": [6426],
    "test_type": "EQUALS",
    "test_values": [""],
    "messages": ["Choose a membership type before saving."]
}
```

Only the first entry of `messages` is used. `target_ids` is not read for anything except deciding how many times the rule body runs, so give it exactly one ID — the trigger's own is the conventional choice.

`ExecuteAllRules` handles the button as a whole rather than rule by rule. It clears every save message, then, if any `disable_save` rule in the ruleset passed, disables the button, writes out each of their messages, and appends a `Validation:` line. If none passed, the button is re-enabled. So the state of the button always reflects the ruleset as a whole, and the messages never accumulate.

That settling happens on **every** `ExecuteAllRules` call, so where a label makes several, the last one decides the button. Keep your `disable_save` rules in the call you make last — production scripts often mark it with a comment saying so — or put them all in a single call.

{% hint style="info" %}
Because the button is settled across the whole ruleset, `disable_save` belongs in an `ExecuteAllRules` call. `ExecuteSingleRule` keeps its own tally, which no other rule reads.

The block also applies to one form in one browser, which is the right scope for guiding somebody filling the form in. Where the same rule has to hold for data arriving any other way, express it in an [on save script](/developer-documentation/case-scripting/on-save-scripts/examples/blocking-a-save-with-validation.md) as well; the two work well together.
{% endhint %}

### hide\_tabs

Hides or shows whole custom field group tabs. `target_ids` here holds **custom field group IDs**, not field IDs, which is the one action where that is true.

```javascript
{
    "action": "hide_tabs",
    "trigger_id": 6502,
    "target_ids": [6512],
    "test_type": "NOT_EQUALS",
    "test_values": ["Yes"]
}
```

Unlike the other actions this one is not sticky: each rule shows or hides its tabs according to its own result, so a later rule can undo an earlier one.

## Test types

`test_type` is either one of these names or a function of your own.

<table><thead><tr><th width="230">Test</th><th>Passes when</th><th>Reads</th></tr></thead><tbody><tr><td><code>EQUALS</code> / <code>NOT_EQUALS</code></td><td>The value matches the test value</td><td><code>test_values[0]</code></td></tr><tr><td><code>IN_LIST</code> / <code>NOT_IN_LIST</code></td><td>The value is one of the test values</td><td>All of <code>test_values</code></td></tr><tr><td><code>CONTAINS</code> / <code>NOT_CONTAINS</code></td><td>The value contains the test value</td><td>The <strong>last</strong> entry of <code>test_values</code></td></tr><tr><td><code>CONTAINS_ANY</code> / <code>NOT_CONTAINS_ANY</code></td><td>The value contains any of the test values</td><td>All of <code>test_values</code></td></tr><tr><td><code>GREATER_THAN</code> / <code>LESS_THAN</code></td><td>The value compares that way</td><td><code>test_values[0]</code></td></tr><tr><td><code>BEFORE</code> / <code>AFTER</code></td><td>The value is a date before or after the test date</td><td><code>test_values[0]</code></td></tr><tr><td><code>ALWAYS</code> / <code>NEVER</code></td><td>Always, or never</td><td>Nothing</td></tr></tbody></table>

Three of these have a particular shape worth knowing.

{% hint style="info" %}
`GREATER_THAN` and `LESS_THAN` compare the values as they come out of the field, which is as strings, so they order alphabetically: `"9" > "10"`. For a numeric comparison, use a [custom test function](#custom-test-functions) and `parseFloat`.

`CONTAINS` tests against the last entry of `test_values`, so give it a single-entry list. `CONTAINS_ANY` is the one that takes several.

`BETWEEN` tests against `test_values[0]` in the same way as `AFTER`. For a two-ended range, use a custom test function with [`test_between`](/developer-documentation/custom-form-scripting/scripting-helper-api-reference.md#dates), which takes both bounds.
{% endhint %}

`ALWAYS` is how a rule is made unconditional, and it is common in rulesets whose decision has already been taken in JavaScript. Such a rule usually has `"trigger_id": null` as well, since there is nothing to test.

### Custom test functions

Anywhere a test name goes, a function goes instead. It is called with the trigger field's value and the rule's `test_values`, and returns `true` or `false`.

```javascript
function retentionEndsBeforeReviewStart(value, testValues) {
    var start = CustomField(6619).Value();
    var end = CustomField(6620).Value();

    if (start === "" || end === "") {
        return false;
    }

    return test_before(end, start);
}

var dateWarning = [{
    "action": "output_messages",
    "trigger_id": 6620,
    "target_ids": [6620],
    "test_type": retentionEndsBeforeReviewStart,
    "test_values": [],
    "messages": ["Retention end date is before the review start date."]
}];
```

Note the function is referenced, not called: `retentionEndsBeforeReviewStart`, not `retentionEndsBeforeReviewStart()`. This is how you compare two fields with each other, where the built-in tests compare one field against a constant.

## How a ruleset is applied

`ExecuteAllRules` evaluates the rules in order and keeps a record as it goes of which fields have been hidden, which disabled, which given a message, and whether the save has been blocked. Rules see that record, which has two consequences worth knowing.

**Hiding is sticky within a pass.** Once any rule has hidden a field, later rules in the same ruleset cannot show it again. This is usually what you want: a field with three reasons to be hidden stays hidden if any of them applies, whatever order the rules are in. The same holds for `disable`.

**The record starts empty on each call.** Splitting rules across two `ExecuteAllRules` calls means the second knows nothing about the first, so a field hidden by the first can be shown again by the second. Keep rules that touch the same fields in one ruleset.

Everything else is applied as it is evaluated, so where two rules do genuinely conflict — two `set_values` writing to the same field — the last one wins.

### Watching it run

Every rule logs its trigger, the value read, the test, the test values and the result to the browser console as it is evaluated:

```
Trigger ID: 6502;Value: No; Test Type: NOT_EQUALS; Test Values: Yes; Result: true
```

That line answers most questions about a ruleset that is not behaving: whether the trigger field was found, what its value actually was, and which way the test went.

## Running one rule

`ExecuteSingleRule(rule)` applies a single rule without an array around it. It is convenient inside a branch of imperative code:

```javascript
if (integrationOn !== "Yes") {
    ExecuteSingleRule({
        "action": "hide",
        "test_type": "ALWAYS",
        "target_ids": [6224]
    });
}
```

It keeps its own record rather than sharing one, so nothing accumulates between calls, and `disable_save` does not work through it. For anything involving the Save button, or several rules that overlap, use `ExecuteAllRules`.

## Re-running rules on change

`ExecuteAllRulesOnChange(rules, id)` binds a change handler to one field so a ruleset is re-applied when that field changes.

{% hint style="info" %}
This dates from a time when rulesets lived in globals with fixed names: the handler re-runs a global called `ruleset3_set_values` rather than the array passed to it, so it suits a script written to that convention and no other. It appears in no live script.

A label re-runs on every change to its form already, which covers most of what this was for. Where you do want a handler tied to one specific field, bind it yourself:

```javascript
CustomField(6112).Input().off('change.rules').on('change.rules', function () {
    ExecuteAllRules(roleLayout);
});
```

Namespace the event and clear it first, since your script runs again on every change and would otherwise add a further copy of the handler each time.
{% endhint %}

## A worked ruleset

Three independent rules in one array, driven by one hidden config label — the standard shape for a conditional form.

```javascript
ThisJSLabel().HideRow();

var retentionLayout = [
    {
        "action": "hide_and_blank",
        "trigger_id": 6502,
        "target_ids": [6505, 6506],
        "test_type": "NOT_EQUALS",
        "test_values": ["Yes"],
        "description": "Special-category retention fields apply only when the flag is Yes"
    },
    {
        "action": "hide_and_blank",
        "trigger_id": 6503,
        "target_ids": [6507, 6508],
        "test_type": "NOT_EQUALS",
        "test_values": ["Yes"],
        "description": "Automated-processing retention fields apply only when the flag is Yes"
    },
    {
        "action": "disable_save",
        "trigger_id": 6502,
        "target_ids": [6502],
        "test_type": "EQUALS",
        "test_values": [""],
        "messages": ["Answer the special category question before saving."]
    }
];

ExecuteAllRules(retentionLayout);
```

## Where to see them used

<table data-view="cards"><thead><tr><th>Example</th><th>What it shows</th><th data-card-target data-type="content-ref">Target</th></tr></thead><tbody><tr><td><strong>Showing and Hiding Dependent Fields</strong></td><td>The smallest useful ruleset.</td><td><a href="/developer-documentation/custom-form-scripting/examples/showing-and-hiding-dependent-fields.md">Showing and Hiding Dependent Fields (Using Rulesets)</a></td></tr><tr><td><strong>Driving a Layout From Several Rules</strong></td><td>One config label, several independent rules.</td><td><a href="/developer-documentation/custom-form-scripting/examples/driving-a-layout-from-several-rules.md">Driving a Layout From Several Rules</a></td></tr><tr><td><strong>Blocking the Save Button</strong></td><td><code>disable_save</code> and its message.</td><td><a href="/developer-documentation/custom-form-scripting/examples/blocking-the-save-button.md">Blocking the Save Button</a></td></tr><tr><td><strong>Comparing Two Dates in a Rule</strong></td><td>A custom test function.</td><td><a href="/developer-documentation/custom-form-scripting/examples/comparing-two-dates-in-a-rule.md">Comparing Two Dates in a Rule</a></td></tr></tbody></table>
