> For the complete documentation index, see [llms.txt](https://api.dataloy.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://api.dataloy.com/user-guides/enterprise-functionality/alert-scripts/script-body-guide-groovy-java.md).

# Script Body Guide (Groovy Java)

This page explains how to write the Script Body for an Alert Script. The script runs server-side, synchronously on a trigger event (Create / Update / Delete) or on a schedule.

The script acts as a **boolean filter**:

* Return `true` → the event is significant and an alert should be generated
* Return `false` → suppress the alert for this event

***

### 1) Script Body rules (must-follow) <a href="#id-1-script-body-rules-must-follow" id="id-1-script-body-rules-must-follow"></a>

* **Groovy only**
* The Script Body is the **inside of a method** \
  ✅ Write statements directly \
  ❌ Do not add a class or method signature
* **Always return a boolean** on every path
* Prefer **early `return false`** for readability and safety

***

### 2) Runtime variables (available without declaration) <a href="#id-2-runtime-variables-available-without-declaration" id="id-2-runtime-variables-available-without-declaration"></a>

These are injected into the script context automatically:

#### **Core context**

* **`dlpObject` (Object)** Current state of the object that triggered the event.
* **`oldDlpObject` (Object)** Previous state of the object **only for UPDATE** events. Often `null` on INSERT, and may not match expected type.
* **`wsUser` (User)** The user/system account that performed the action. Useful for role-based suppression.

#### **Message output**

* **`mapResultsForMessage` (Map\<String, String>)** Placeholder map for template-based message text. Typical keys are `"?1"`, `"?2"` etc.
* **`mapMessageFromServer` (Map\<String, String>)** Full message override. Use key `"MSG"` to provide the message text.

**Scheduled scripts**

A scheduled run is not caused by a change, so there is no triggering object.

* **`dlpObject` / `oldDlpObject`** Not available. Referencing them makes the script fail and no alert is sent.
* **`ctx` (ObjectContext)** Read-only data context. Use it to query the data the alert should check (see section 10).
* **`wsUser`**, **`mapResultsForMessage`**, **`mapMessageFromServer`** Available as for event scripts.

{% hint style="warning" %}
A script that contains the text `oldDlpObject` anywhere, including in a comment, cannot be scheduled. Use separate scripts for event-based and scheduled alerts.
{% endhint %}

***

### 3) Recommended structure (copy-paste mental model) <a href="#id-3-recommended-structure-copy-paste-mental-model" id="id-3-recommended-structure-copy-paste-mental-model"></a>

A clean script usually follows this flow:

1. **Imports** (only what you use)
2. **Type check + cast** `dlpObject` (and optionally `oldDlpObject`)
3. **Guard clauses** for nulls / wrong event shape
4. **Business logic** (status checks, threshold checks, role checks, etc.)
5. **Populate message values** (template placeholders or full message)
6. `return true`

***

### 4) Type check + casting (avoid ClassCastException) <a href="#id-4-type-check--casting-avoid-classcastexception" id="id-4-type-check--casting-avoid-classcastexception"></a>

`dlpObject` and `oldDlpObject` are generic `Object`, so type-check first.

```groovy
import com.dataloy.ds.Voyage
if (!(dlpObject instanceof Voyage)) 
    return false Voyage current = (Voyage) dlpObject
```

For old object (updates only):

```groovy
Voyage old = 
    (oldDlpObject instanceof Voyage)
     ? (Voyage) oldDlpObject
     : null
```

***

### 5) Null-Safety: Patterns and Best Practices <a href="#id-5-null-safety-patterns-dont-catch-npes-with-your-face" id="id-5-null-safety-patterns-dont-catch-npes-with-your-face"></a>

Assume linked objects can be null (headers, vessel, statuses, etc.). Use:

* **Guard clauses**
* Groovy **safe navigation** `?.`
* Null coalescing `?:`

```groovy
def vesselName = vessel?.vesselName
// vessel is null, so "Unknown vessel" is returned
def displayName = vesselName ?: "Unknown vessel"

def refNo = current?.voyageHeader?.referenceNo 
if (!refNo) 
    return false
```

***

### 6) Field access and status comparisons

#### Linked object navigation

```groovy
def statusCode = current?.voyageHeader?.voyageStatus?.statusTypeCode
```

#### Status fields

Compare **status codes**, not the status object:&#x20;

```groovy
if (!"OPR".equals(statusCode)) 
    return false
```

{% hint style="info" %}
Tip: `"LITERAL".equals(x)` is null-safe and avoids surprises.
{% endhint %}

***

### 7) Update/change detection (current vs old) <a href="#id-7-update-change-detection-current-vs-old" id="id-7-update-change-detection-current-vs-old"></a>

If your trigger is UPDATE and you only want alerts on change:

#### **Step-by-step pattern**

```groovy
if (old == null) return false // not an update or no previous state
```

#### **Null-safe comparisons**

Avoid calling `.equals()` on potentially null values.

```groovy
def newVal = current?.someField 
def oldVal = old?.someField
if (newVal == null || oldVal == null) return false 
if (newVal == oldVal) return false 
```

#### **Numeric thresholds (example)**

```groovy
import java.math.BigDecimal
BigDecimal newPnl = current?.voyageResult as BigDecimal 
BigDecimal oldPnl = old?.voyageResult as BigDecimal 
if (newPnl == null || oldPnl == null) return false
if ((newPnl - oldPnl).abs() <= 20000G) return false
```

***

### 8) Message building modes (CRITICAL – prevents misconfiguration)

There are **two valid combinations** between the Script Body and Message Configuration.

#### **Mode A — Build message in script = true**

Use this when the Script Body produces the full message text.**Requirements**

* `Build message in script = true`
* Script must set: `mapMessageFromServer["MSG"]`

{% code overflow="wrap" %}

```groovy
mapMessageFromServer.put("MSG", "Voyage PNL changed: " + current?.voyageHeader?.referenceNo) 
return true
```

{% endcode %}

#### **Mode B — Build message in script = false**

Use this when the server builds the message using a template string (`messageTxt`) and placeholder map.

**Requirements**

* `Build message in script = false`
* `messageTxt` must be provided (example: `"Hello ?1"`)
* Script sets: `mapResultsForMessage["?1"] = "World"`

Example configuration:

* `messageTxt = "Hello ?1"`

**Script:**

```groovy
mapResultsForMessage.put("?1", "World")
return true
```

#### **The broken combo**

**Don’t do this:**

* `mapResultsForMessage` + `Build message in script = true`

Reason: `Build message in script = true` expects `"MSG"` in `mapMessageFromServer`.

***

### 9) Guidance for generators / validation logic (recommended) <a href="#id-9-guidance-for-generators-validation-logic-recommended" id="id-9-guidance-for-generators-validation-logic-recommended"></a>

When auto-generating alert script configs, set `Build message in script` using this rule:

1. If Script Body contains `mapMessageFromServer` → set **true**
2. Else if Script Body contains `mapResultsForMessage` AND `messageTxt` is set → set **false**
3. Else → fail validation or require the missing pieces (recommended)

This prevents “alerts firing with empty messages” situations.

***

### 10) Optional: fetching related data (advanced) <a href="#id-10-optional-fetching-related-data-advanced" id="id-10-optional-fetching-related-data-advanced"></a>

Sometimes the needed data is not reachable from `dlpObject` (link not loaded / not present). You can query the database using:

```groovy
com.dataloy.platform.query.DlpObjectSelect
```

{% hint style="warning" %}
Method details can vary by version and are not fully documented. Use known examples as reference and test carefully.
{% endhint %}

In scheduled scripts, querying is the only way to reach data. Pass `ctx` to `select`:

```groovy
import com.dataloy.ds.*
import com.dataloy.platform.query.DlpObjectSelect
import org.apache.cayenne.exp.ExpressionFactory

List<PortCall> notFixed = DlpObjectSelect.query(PortCall.class)
        .where(ExpressionFactory.matchExp(PortCall.ARRIVAL_FIXED_PROPERTY, false))
        .select(ctx)

if (notFixed.isEmpty()) return false

mapMessageFromServer.put("MSG", notFixed.size() + " port calls with arrival not fixed")
return true
```

{% hint style="info" %}
Scripts are read-only. A script containing `getThreadDataContext` or `commitChanges` is rejected when it runs, and no alert is sent.
{% endhint %}

***

### 11) Troubleshooting <a href="#id-11-troubleshooting" id="id-11-troubleshooting"></a>

#### **Logging**

Print debug information to application logs:

```groovy
java.lang.System.out.println("---- newPnl: " + newPnl)
```

#### **Return-path sanity**

If alerts don’t fire:

* confirm type-check isn’t rejecting
* confirm `oldDlpObject` isn’t null when you expect update logic
* confirm [message building](https://app.gitbook.com/o/-LhoT2vqihl0pYiCeolt/s/7R7IFboJW2JabvnivFfc/~/edit/~/changes/32/voyage-management-system/step-by-step-guides/alerts/alert-scripts/script-body-groovy-detailed-guide#id-8-message-building-modes-critical-prevents-misconfiguration) is valid

If it's a scheduled script:

* confirm it doesn't use `dlpObject` / `oldDlpObject`, and that it sets a message (Message Text or `mapMessageFromServer`). Scheduled runs have no default message, so a script returning `true` without one sends nothing
* alerts firing outside the schedule: remove the script's Object Type / Event Type triggers. A scheduled script with triggers runs both on schedule and on every matching change

***

### 12) Full example (safe, readable, and template-based) <a href="#id-12-full-example-safe-readable-and-template-based" id="id-12-full-example-safe-readable-and-template-based"></a>

Config:

* `Build message in script = false`
* `messageTxt = "Voyage ?1 PNL changed to ?2"`

Script:

```groovy
import com.dataloy.ds.Voyage 
import java.math.BigDecimal

if (!(dlpObject instanceof Voyage)) return false 
Voyage current = (Voyage) dlpObject

Voyage old = (oldDlpObject instanceof Voyage) ? (Voyage) oldDlpObject : null 
if (old == null) return false

BigDecimal newPnl = current?.voyageResult as BigDecimal 
BigDecimal oldPnl = old?.voyageResult as BigDecimal 
if (newPnl == null || oldPnl == null) return false

if ((newPnl - oldPnl).abs() <= 20000G) return false

mapResultsForMessage.put("?1", current?.voyageHeader?.referenceNo ?: "") 
mapResultsForMessage.put("?2", String.valueOf(newPnl))
return true
```


---

# 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 by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://api.dataloy.com/user-guides/enterprise-functionality/alert-scripts/script-body-guide-groovy-java.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

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.
