> 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/case-scripting/on-save-scripts/examples/blocking-a-save-with-validation.md).

# Blocking a Save With Validation

Returning `false` from `on_save` abandons the save. The user's edits are not written, everything the script had queued is dropped, and any message the script added is still shown. This is the one thing an on save script can do that nothing else can, and it is the reason validation that genuinely has to hold belongs here rather than in a form script.

Use it sparingly. A refused save loses the user's typing, so it should be reserved for values that would leave the record wrong rather than merely untidy.

## The scenario

A data processing record must not show processing that began on or before the date consent was given, nor before the retention period started. Both would make the record indefensible, so neither should be storable. Once a valid start date is saved, the data protection officer is told, except for internal and staff-only processing which they do not need to see.

| Setting       | Value                                                                                                               |
| ------------- | ------------------------------------------------------------------------------------------------------------------- |
| Trigger field | `5602` Processing start date                                                                                        |
| Read          | `5603` Consent required, `5604` Retention start date, `5605` Consent date, `5606` Processing type, `5607` DPO email |
| Exempt types  | `INTERNAL`, `STAFF-ONLY`                                                                                            |
| Template      | `DPO-ProcessingStarted`                                                                                             |

## The script

```javascript
[Changed.CustomField(5602).Changed]

function on_save() {
    if (Service.CustomField("5603").ValueAsText !== "Yes") {
        return true;
    }

    var processing = Changed.CustomField("5602").NewValueAsText;
    var consent = Service.CustomField("5605").ValueAsText;
    var retention = Service.CustomField("5604").ValueAsText;

    if (processing === "") {
        return true;
    }

    if (consent !== "" && Service.DateDiff(consent, processing) <= 0) {
        Service.AddMessage(
            "This cannot be saved, because processing cannot start on or before the date consent was given.",
            "error");
        return false;
    }

    if (retention !== "" && Service.DateDiff(retention, processing) < 0) {
        Service.AddMessage(
            "This cannot be saved, because processing cannot start before the retention period does.",
            "error");
        return false;
    }

    return true;
}

function after_save() {
    var processingType = Service.CustomField("5606").ValueAsText;

    if (processingType === "INTERNAL" || processingType === "STAFF-ONLY") {
        return true;
    }

    if (Service.CustomField("5607").ValueAsText === "") {
        return true;
    }

    Service.SendEmail("DPO-ProcessingStarted");
    return true;
}
```

## Checking out before checking in

The script rules itself out before it does any work. Processing that needs no consent is not subject to these rules at all, and a cleared start date has nothing to compare, so both return `true` immediately.

Each comparison then guards the field it depends on. A case with no consent date recorded is incomplete rather than invalid, so the script lets it save rather than blocking on a date nobody has entered yet. Deciding this explicitly is what stops a validator from becoming an obstacle: a script that refuses every save until every field is populated makes a case impossible to build up gradually.

## Comparing the dates

`Service.DateDiff(startDate, endDate)` returns the whole days between two dates, negative when the second is earlier. That makes the two rules read directly: consent must be strictly before processing, so a difference of zero or less is a failure, while retention may begin on the same day, so only a negative difference is.

{% hint style="warning" %}
`DateDiff` reads slash and hyphen dates day-first, so `01/02/2026` is 1 February. It also throws if a value cannot be read as a date at all, which shows the user an error and stops the script. Check for an empty value before calling it, as above, and be wary of dates from a field users can type freely into.
{% endhint %}

If you need the dates as JavaScript objects rather than a difference, build them from their parts rather than passing the text to `new Date`, which reads an ambiguous date as month-first:

```javascript
var parts = processing.split("/");
var processingDate = new Date(Number(parts[2]), Number(parts[1]) - 1, Number(parts[0]));
```

The month is one less than you would write, because JavaScript numbers months from zero.

## Writing the message

The message is what the user is left with, since their change has gone. It should say that nothing was saved and give the reason precisely enough to act on, which is why these use `error` to force a dialog rather than a notification that fades before it is read.

Compare "Invalid date" with "processing cannot start on or before the date consent was given". The second tells the user which two fields are in conflict and which way round they should be, so the next save is likely to be the right one.

## The two functions doing different jobs

`on_save` decides whether the save may proceed and sends nothing. `after_save` runs only if it did, and only then sends the notification.

Splitting them this way is what makes the outcome trustworthy. There is no path where the officer is told processing has started on a case that failed to save, and because the email goes from `after_save` its merge fields render against the stored start date rather than the one it replaced.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.agilecase.com/developer-documentation/case-scripting/on-save-scripts/examples/blocking-a-save-with-validation.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
