> For the complete documentation index, see [llms.txt](https://docs.openiam.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.openiam.com/developer-guide/batch-scripts.md).

# Batch scripts

Scheduled scripts that run on their own — the context binding, reaching services, the output variable, and an annotated shipped example.

A batch script runs on a schedule, on its own. Nothing is handed to it — no row, no diff, no object — so it starts by finding what it needs to work on.

**Batch scripts are not classes.** Every other script type in this guide is a class extending a base class or implementing an interface. A batch script is a plain Groovy script: statements at the top level, executed in order. **Carry the class habit over from the other types and nothing runs, with no error to read.**

## What this builds, and how long it takes

|                |                                                                                  |
| -------------- | -------------------------------------------------------------------------------- |
| **You get**    | A script that runs on a schedule and reports success or failure to the scheduler |
| **Time**       | An hour, including one manual run before scheduling                              |
| **You need**   | A deployment, and the ability to create a scheduled task                         |
| **Reversible** | Yes — disable the scheduled task. **What the script already did is not**         |

## Before you start

* [*Set up your environment*](/developer-guide/set-up-your-environment.md) and [*How scripting works*](/developer-guide/how-scripting-works.md).
* **A bound on what it will act on.** A batch script with no limit, run against a full user population at three in the morning, is the kind of thing that is discovered the next day.

***

## Build it

### 1. Learn the two conventions

**The Spring context arrives as a binding variable called `context`.** Cast it, then pull the beans you need:

```groovy
import org.springframework.context.ApplicationContext
import org.openiam.common.beans.mq.UserRabbitMQService

ApplicationContext appContext = (ApplicationContext) context
def UserRabbitMQService userManager = appContext.getBean(UserRabbitMQService.class)
```

**The result is a variable called `output`.** Set it as the last statement:

```groovy
output = 0
```

**`0` means success.** A non-zero value marks the task failed, and the scheduler records it. **Set `output` on every path** — a script that returns without setting it has an undefined result.

### 2. Know which services are available

The shipped batch scripts reach these through `appContext.getBean(...)`:

| Service               | For                                     |
| --------------------- | --------------------------------------- |
| `UserRabbitMQService` | Finding and updating users              |
| `LoginMQService`      | Logins, including last-login timestamps |
| `ProvisionMQService`  | Provisioning and de-provisioning        |
| `MailRabbitMQService` | Sending notifications                   |
| `RoleRabbitMQService` | Role membership                         |

### 3. Write one that only logs

```groovy
import org.openiam.common.beans.mq.UserRabbitMQService
import org.springframework.context.ApplicationContext

ApplicationContext appContext = (ApplicationContext) context
def UserRabbitMQService userManager = appContext.getBean(UserRabbitMQService.class)

int outVal = 1
try {
    println("housekeeping: starting")
    // the work goes here
    outVal = 0
} catch (Exception e) {
    println("housekeeping: failed - " + e.getMessage())
}
output = outVal
```

**Start with a script that only logs.** Schedule it, confirm it runs on the schedule and that its output reaches the log, and only then give it something to do. Scheduling problems and logic problems look alike otherwise.

### 4. Read a shipped example

From `conf/iamscripts/batch/inactiveUser_1.0.groovy`, abridged. It finds users who have not signed in for ninety days:

```groovy
import org.openiam.dto.Login
import org.openiam.common.beans.mq.LoginMQService
import org.openiam.common.beans.mq.MailRabbitMQService
import org.openiam.common.beans.mq.ProvisionMQService
import org.openiam.common.beans.mq.UserRabbitMQService
import org.springframework.context.ApplicationContext

ApplicationContext appContext = (ApplicationContext) context

def UserRabbitMQService userManager  = appContext.getBean(UserRabbitMQService.class)
def LoginMQService loginManager      = appContext.getBean(LoginMQService.class)
def MailRabbitMQService mailService  = appContext.getBean(MailRabbitMQService.class)
def ProvisionMQService provision     = appContext.getBean(ProvisionMQService.class)

int outVal = 1
final int INACTIVE_DAYS = 90

def boolean isIdmAccountActive(Login lg, LoginMQService loginManager, int days) {
    Login idmLg = loginManager.getLoginByManagedSys(lg.login, "0").getValue()
    if (idmLg == null)           { return false }
    if (idmLg.lastLogin == null) { return false }
    long lastLoginMilis = idmLg.lastLogin.getTime()
    long curTimeMillis  = new Date().getTime()
    // ... compare against days
}

// ... find candidates, notify, set outVal = 0 on success

output = outVal
```

**Five things to take from it:**

* **`outVal` starts at `1`.** It becomes `0` only once work has succeeded, so a script that fails early reports failure by default. The alternative silently passes.
* **The threshold is a named constant.** `INACTIVE_DAYS` at the top, not `90` buried in a comparison. Batch scripts are read by whoever is investigating why something was disabled.
* **Methods can be declared inside the script.** `def boolean isIdmAccountActive(...)` is a normal Groovy method at the top level. You do not need a class to organise the logic.
* **`"0"` is the managed system id for OpenIAM itself.** It appears throughout the shipped scripts as the way to ask for the OpenIAM-side login rather than a target's.
* **`getValue()` unwraps the response.** The MQ services return a wrapper; the object is inside it. Forgetting this gives a type error that reads oddly.

### 5. Deploy it

1. Groovy Manager → the `batch/` folder.
2. **New Script**, **Save**. It compiles on save.
3. **Run it once by hand before scheduling it.** A batch script that fails on a schedule at three in the morning is harder to read than one that fails in front of you.
4. Attach it to a scheduled task.

***

## Verify it

| Check              | Where                                                |
| ------------------ | ---------------------------------------------------- |
| It ran             | The batch task's history                             |
| It succeeded       | The recorded result. `output = 0`                    |
| What it did        | Its own `println` and log output, in the service log |
| It ran on schedule | Two consecutive runs at the expected interval        |

**Check the second run, not just the first.** A script that leaves state behind can succeed once and fail after.

## If it goes wrong

| What you see                                    | What it means                                               | What to do                                                               |
| ----------------------------------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------------ |
| Nothing happens, no error                       | Written as a class. Top-level statements never run          | Remove the class wrapper. Batch scripts are plain scripts                |
| `groovy.lang.MissingPropertyException: context` | The binding is not available in that execution context      | Confirm it is attached as a batch task, not run from another type's hook |
| The task always reports failure                 | `output` is never set to `0`, or never set at all           | Set it on the success path, and check every early return                 |
| The task reports success but did nothing        | `output = 0` set before the work, or an exception swallowed | Set it after the work; log in the catch block                            |
| `NullPointerException` on a service call        | The response wrapper was not unwrapped                      | Add `.getValue()`                                                        |
| It runs twice                                   | Attached to more than one scheduled task                    | Check the task list                                                      |
| It ran a long time and was still going          | No bound on the working set                                 | Add a limit, and log a count at the start                                |

## What this procedure does not cover

Creating and scheduling the task itself, which is the [Administration Guide](/administration-guide/admin.md)'s subject. The scripts that ship for common jobs — reconciliation, synchronisation, expiry and notification — are worth reading before writing a new one; several of the jobs people write already exist.

## Related

* [*Reconciliation scripts*](/developer-guide/reconciliation-scripts.md) — commonly triggered from a batch task
* [*How scripting works*](/developer-guide/how-scripting-works.md)

*Applies to OpenIAM 2026.8.1.*


---

# 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://docs.openiam.com/developer-guide/batch-scripts.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.
