> 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/examples/comparing-two-dates-in-a-rule.md).

# Comparing Two Dates in a Rule

A custom test function, for a condition the built-in tests cannot express.

The built-in test types all compare one field against a constant. Comparing two fields with each other needs a function — and `test_type` accepts one anywhere a test name would go.

## The scenario

A GDPR record holds a review start date and a retention end date. Retention ending before the review begins is not a valid combination, and it is the sort of thing that comes from a mistyped year.

| Setting      | Value                                               |
| ------------ | --------------------------------------------------- |
| Config label | `6618` Retention date check, hidden                 |
| Fields       | `6619` Review start date, `6620` Retention end date |

## The script

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

function endsBeforeStart() {
    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": endsBeforeStart,
    "test_values": [],
    "messages": ["The retention end date is before the review start date."],
    "description": "Retention cannot end before the review begins"
}];

ExecuteAllRules(dateWarning);
```

## Referenced, not called

`"test_type": endsBeforeStart` passes the function itself. Writing `endsBeforeStart()` would call it once, while the ruleset was being built, and store its answer — a rule that is permanently true or permanently false, and looks identical on the page.

The engine calls it with the trigger field's value and the rule's `test_values`. This function ignores both and reads what it needs itself, which is the usual shape when the point is to compare two fields.

## Handling the empty case

The early return matters. A form starts with both dates empty, and gets to a state where one is filled in and the other is not on the way to being complete. Neither is an error, and a warning shown while somebody is still typing is a warning they learn to ignore.

`test_before` returns `false` for a date it cannot read, so the check is not strictly needed. Writing it anyway states the intention — a missing date is not a mistake — rather than leaving it implied.

## Dates in either form

`test_before` and `test_after` parse both forms an AgileCase date takes: `DD/MM/YYYY` as displayed, `YYYYMMDD` as stored. That is what makes them safe to use on a value from `Value()` without knowing which one you have.

They are worth using in preference to comparing the strings directly, since `<` on two dates orders them as text — `01/04/2026` sorts before `02/01/2020`, the first characters deciding it — where `test_before` parses both sides as dates.

{% hint style="info" %}
The `BETWEEN` test type tests against `test_values[0]` in the same way as `AFTER`. For a two-ended range, `test_between(value, from, to)` takes both bounds, and a custom function is how you reach it.
{% endhint %}

## Warning, not blocking

`output_messages` displays the problem and lets the save go ahead. That suits a combination that is usually wrong but occasionally deliberate.

To stop the save, add a [`disable_save`](/developer-documentation/custom-form-scripting/examples/blocking-the-save-button.md) rule with the same test function to the array — the function can be shared between rules, so the two cannot drift apart:

```javascript
{
    "action": "disable_save",
    "trigger_id": 6620,
    "target_ids": [6620],
    "test_type": endsBeforeStart,
    "test_values": [],
    "messages": ["Fix the retention dates before saving."]
}
```

## Reusing the test values

A custom function is also a way to make a comparison numeric rather than textual, which the built-in `GREATER_THAN` is not:

```javascript
function overThreshold(value, testValues) {
    return parseFloat(value) > parseFloat(testValues[0]);
}
```

Written this way the function uses both arguments the engine passes it, so the same function serves any rule that names a threshold in its `test_values`.
