> 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/administration-guide/automation/batch-tasks.md).

# Create a batch task

Create, parameterize, test and schedule a batch task — the Groovy bindings, the Spring-bean route, the real validation errors and the two timers that decide when it runs.

## What this is, and when you need it

A **batch task** is one `BATCH_CONFIG` row that runs either a Groovy script or a product-provided method, once on a date or on a cron schedule. Scheduling and execution belong to the dedicated **batch-task-manager** service, not to the ESB. Manage them at **Administration → Batch Tasks** — `/webconsole/batchTaskSearch` for the list and `/webconsole/batchTask?id=<TASK_ID>` for one task.

First check that a shipped task does not already own the job. **Fifteen named batch tasks ship, and four ship enabled**: `LEAVER_PROCESS` at 17:00, `PROV_BY_START_DATE_FUT` and `CLEAN_AUTH_STATE_TABLE` at midnight, `RISK_NIGHTLY_SWEEP` at 02:00. Everything else is off until an administrator enables it — including `PRUNE_AUDIT_LOGS`, `REQUEST_REMINDERS`, `REQUEST_ESCALATION`, `AUTO_REVOCATION` and `PERFORM_BR_REPROVISION`. The full inventory is [Shipped batch tasks](/administration-guide/reference/shipped-batch-tasks.md).

## At a glance

|                          |                                                                                                                                                                     |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **You will end up with** | One saved, enabled `BATCH_CONFIG` row that has run once on demand and produced a `BATCH_TASK_EXECUTE` audit record you have read                                    |
| **Where**                | `/webconsole/batchTaskSearch` — **Administration → Batch Tasks → New**                                                                                              |
| **You need first**       | The Groovy script already saved and compiling in Groovy Manager, or a `bean.method` pair from the Webconsole's own dropdown; and a bounded target to run it against |
| **Takes**                | 15 minutes to create and run once; up to 5 minutes more before the schedule is picked up                                                                            |
| **Reversible**           | Partly — the task is, its effects are not. Deleting a task reverses nothing a run already did                                                                       |

## Before you begin

* **A disposable target** — one test managed system, one synchronization config over a three-row CSV, one test user. The first run of a batch task is the widest single action in OpenIAM administration.
* **For the script route:** the script saved and compiling in Groovy Manager, under a path you own — see [Create a Groovy script](/administration-guide/automation/manage-groovy-scripts.md).
* **For the bean route:** a `bean.method` pair from the Webconsole's dropdown. You cannot type a bean name, and only thirteen pairs are offered — they are listed in [Shipped batch tasks](/administration-guide/reference/shipped-batch-tasks.md#the-thirteen-executable-beans).

{% hint style="danger" %}
One shipped row must never be enabled: `TASK_EMAIL_NOTIFICATION` names a bean the batch-task-manager service does not contain, so the Webconsole refuses to re-save it and enabling it produces a failed run. See [Shipped batch tasks](/administration-guide/reference/shipped-batch-tasks.md#the-row-that-cannot-run).
{% endhint %}

## Choose the execution type

The **Execution Script** dropdown offers exactly two values, **Groovy Script** and **Execute a Spring Bean**, and they map to two mutually exclusive column sets on `BATCH_CONFIG`:

| Column                           | Field                                      | Set when                                |
| -------------------------------- | ------------------------------------------ | --------------------------------------- |
| `GROOVY_SCRIPT_ID`               | Groovy Script URL                          | Execution type is Groovy Script         |
| `SPRING_BEAN` (100 chars)        | Spring Bean Name                           | Execution type is Execute a Spring Bean |
| `SPRING_BEAN_METHOD` (100 chars) | the second half of the same dropdown value | Execution type is Execute a Spring Bean |

The dropdown shows one entry per pair, labelled `<beanId>.<methodName>`, and splits it on the first `.` when saving. If a script is set OpenIAM clears the bean and the method before storing the row, and at run time the script wins.

Choose the bean route only for a product-provided method. A custom compiled task is a **Developer Guide** task, not a Webconsole one.

## Create a batch task

{% stepper %}
{% step %}

### Check that a shipped task does not already own the job

Compare against [Shipped batch tasks](/administration-guide/reference/shipped-batch-tasks.md) first. Do not build a second scheduler around synchronization or reconciliation — use `batch/synchronization.groovy` or `batch/reconciliation.groovy` and pass the configuration ID as **Parameter 1** — which has a page of its own: [Run a synchronization on a schedule](/administration-guide/automation/batch-tasks/schedule-a-synchronization.md).
{% endstep %}

{% step %}

### Save the script in Groovy Manager

Under your own path, for example `customer/batch`, not under `batch/`. See [Create a Groovy script](/administration-guide/automation/manage-groovy-scripts.md). The task stores the script's **ID**, so the version you select is pinned — a later branch does not become active on its own.
{% endstep %}

{% step %}

### Create the task

**Administration → Batch Tasks → New**. Fill in:

* **Task Name** — required (`Task name is invalid or missing` if blank). Help text: *"The unique name of this task"*. Make it identify purpose and environment scope.
* **Is Active** — the `ENABLED` column. See the step on testing below for what it does and does not gate.
* **Select an Execution Time** — **Cron Job** or **Execute once on exact date**. Exactly one; the service nulls the other. Both blank fails with `No Execution Time Provided. Please provide either a Cron Expression or Execution Date.`
* **Cron Expression** or **Run On**.
* **Execution Script** — **Groovy Script** (then pick the script) or **Execute a Spring Bean** (then pick a `bean.method` from the dropdown).
* **Parameter 1**–**Parameter 4** — help text *"Optional parameter, which will be used by the Groovy Script or Spring Bean"*. Never a secret: these are plain 255-character columns and they are written to the audit record of every save.
  {% endstep %}

{% step %}

### Save and read the error, if any

Saving validates the row and maps each failure to a specific message. These are the real response codes and the exact text you will see:

| Response code                    | Message                                                                                                                                                         |
| -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `NO_NAME`                        | *Task name is invalid or missing*                                                                                                                               |
| `NO_EXEUCUTION_TIME`             | *No Execution Time Provided. Please provide either a Cron Expression or Execution Date.*                                                                        |
| `INVALID_CRON_EXRPESSION`        | *Cron Expression Invalid*                                                                                                                                       |
| `DATE_INVALID`                   | *Execution Date Invalid. It should be after the current date.*                                                                                                  |
| `SPRING_BEAN_OR_SCRIPT_REQUIRED` | *Provide a Groovy script or Spring Bean*                                                                                                                        |
| `FILE_DOES_NOT_EXIST`            | *Groovy Script does not exist*                                                                                                                                  |
| `INVALID_SPRING_BEAN`            | *Spring Bean is invalid. Either the bean does not exist, the method is missing, or the method is invalid. Please note that only no-argument methods are valid.* |

Two of them are worth understanding rather than just reading:

* **`FILE_DOES_NOT_EXIST`** means the script **ID** did not resolve — a stale ID after a restore, or a script deleted from under the task. It is not about a missing file on disk.
* **`INVALID_SPRING_BEAN`** compares your bean and method against the thirteen offered pairs exactly. The message's closing sentence about no-argument methods is misleading: methods taking one string are supported, and that is how `param1` is passed.

Success is *Batch Task was saved successfully* and a redirect to `batchTask?id=<TASK_ID>`.
{% endstep %}

{% step %}

### Test with **Execute**, before scheduling

**Execute** runs the task once, asynchronously, **regardless of the Enabled flag**. The message is *Batch Task successfully scheduled to run asynchronously* — that is the queue accepting the work, not the work succeeding.

**Schedule** is different: the button only renders when the task is saved **and** enabled.

So the safe order is: save disabled → **Execute** once against a bounded target → inspect the outcome → then enable.
{% endstep %}

{% step %}

### Enable, and know when the schedule is picked up

Two independent timers in the batch-task-manager service materialise and then run the schedule rows, and their intervals decide two things worth knowing: a **cron change can take up to five minutes to take effect**, and a **disabled task is never swept**, so disabling removes future scheduling but leaves any already-materialised schedule row, which the 20-second timer will still run. Disabling is not an emergency stop. Both properties and the full mechanics are in [How the schedule table works](/administration-guide/automation/batch-tasks/diagnose-stalled-batch-tasks.md#how-the-schedule-table-works).
{% endstep %}
{% endstepper %}

## The Groovy route: the exact bindings

The script route runs your script as a **script body**, not as a class, so its last expression is the return value. The binding map is these eight entries and nothing else:

| Binding           | Type                        | What it is                                                                                               |
| ----------------- | --------------------------- | -------------------------------------------------------------------------------------------------------- |
| `context`         | Spring `ApplicationContext` | The batch-task-manager context. `context.getBean(SomeMQService.class)` is how you reach every service    |
| `taskObj`         | The task row                | `taskObj.id`, `taskObj.name`, `taskObj.cronExpression`, `taskObj.lastExecTime`                           |
| `lastExecTime`    | `java.util.Date`            | The previous successful start time, or `null` on the first run. This is your incremental-window boundary |
| `parentRequestId` | `String`                    | A fresh UUID per run, for correlating whatever you initiate                                              |
| `param1`–`param4` | `String`                    | `BATCH_CONFIG.PARAM1`–`PARAM4`, 255 chars each. All four reach a script, unlike the bean route           |

`context` is injected by the script engine as well, so it is present even when the binding map does not carry it.

**Cast a binding before you operate on it.** The table's types are what the caller puts in the binding map, not what the compiler sees. Every binding reaches the script as `Object`, so `param1 + param2` does not compile — write `(param1 as Integer) + (param2 as Integer)`. Since 2026.8.1 this is enforced on the execution path too, not only when you save; see [The sandbox on the execution path](/administration-guide/reference/groovy-script-reference.md#the-sandbox-on-the-execution-path).

**The return contract.** The result of the script is required to be an `Integer`: **`0` is success, any other integer is a handled failure**, and a result of any other type fails the run with `Cannot execute batch groovy task` and audits it as a failure. The shipped convention — used by `batch/synchronization_1.0.groovy`, `batch/reconciliation_1.0.groovy`, `batch/inactiveUser_1.0.groovy` and `batch/deleteOnExpiration_1.0.groovy` — is to end the script with `output = 0`, whose value as an expression is `0`.

{% hint style="info" %}
**The bean route passes one parameter, not four.** OpenIAM chooses the method by whether **Parameter 1** is populated: with `PARAM1` empty it looks for a no-argument method, and with `PARAM1` set it looks for one that takes a single string. So putting a value in **Parameter 1** for a no-argument method makes the lookup find nothing, the run does nothing, and `LAST_EXEC_TIME` never advances — with no error to read. `PARAM2`–`PARAM4` never reach a bean at all. All four reach a script.
{% endhint %}

`LAST_EXEC_TIME` is updated to the run's **start** time after the run returns, on both the script and the bean path, and only if the row still exists. It is not updated when the script throws.

### Searching for users from a batch script

Almost every batch script starts by finding a population, and a `UserSearchBean` passed to the user service's `findBeans` is how. Two things about that search cost a cycle each if you meet them the hard way.

**Declare types explicitly; do not lean on `def`.** A script written with `def` throughout compiles and then fails inside the service with messages that name the wrong object — `No such property: getMessage for class: java.lang.Object` is the one seen most. The shipped `failedProvisionRequestReport_1.0.groovy` declares everything, as in `String manSysIdsParam = (String) param1`. Do the same, and import from the package the class actually lives in — `org.openiam.common.beans.mq.*` for the MQ services.

**Do not filter by employee type against the database.** The search bean carries employee type as a plain string, while OpenIAM stores a user's employee type as a reference to a metadata type. The database search path therefore hands the persistence layer a string where it expects an entity, and the search fails — whatever else is in the search bean — with:

{% code overflow="wrap" %}

```
org.springframework.dao.InvalidDataAccessApiUsageException: Parameter value [<EMPLOYEE_TYPE_ID>] did not match expected type [org.openiam.idm.srvc.meta.domain.MetadataTypeEntity (n/a)]
```

{% endcode %}

Because the search crosses the message bus, the line above it in the log is the generic `Exception Occurred during RabbitMQ Call`, which says nothing about the cause. Read the `InvalidDataAccessApiUsageException` line.

The workaround is one setter on the search bean: `useElasticSearch`. The search-index path matches the same value as an exact match on a string field, so the filter works there.

{% code overflow="wrap" %}

```groovy
UserSearchBean usb = new UserSearchBean()
usb.setEmployeeType("<EMPLOYEE_TYPE_ID>")
usb.setUseElasticSearch(true)          // the DB path cannot filter on this field
```

{% endcode %}

{% hint style="warning" %}
The search-index path is only as current as the index. For a nightly job that is usually acceptable; for a job that must see a change made seconds earlier it is not. When you need database-accurate filtering, select the population on a criterion the database path does handle and test the employee type on each returned user in the script. Raise the type mismatch with support rather than reaching for SQL.
{% endhint %}

## A complete batch-task script

Adapted from the shipped `ActivateProcess_1.0.groovy` — same job (activate users whose start date has arrived, then send them an activation link) — rewritten as a **script body** for the script route, with every service resolved through `context` so no inherited field can be null. The structure of the shipped `batch/synchronization_1.0.groovy` (cast `context`, get beans, read `param1`, end with `output = 0`) is preserved exactly.

`param1` is the number of days ahead to look; leave it blank for `1`.

{% code overflow="wrap" lineNumbers="true" expandable="true" %}

```groovy
import org.openiam.base.request.NotificationParam
import org.openiam.base.request.PasswordResetTokenRequest
import org.openiam.base.response.PasswordResetTokenResponse
import org.openiam.base.ws.DateSearchParam
import org.openiam.base.ws.MatchType
import org.openiam.common.beans.mq.MailRabbitMQService
import org.openiam.common.beans.mq.PasswordMQService
import org.openiam.common.beans.mq.ProvisionMQService
import org.openiam.common.beans.mq.UserRabbitMQService
import org.openiam.dto.Login
import org.openiam.dto.User
import org.openiam.idm.searchbeans.UserSearchBean
import org.openiam.idm.srvc.user.dto.UserCollection
import org.openiam.idm.srvc.user.dto.UserStatusEnum
import org.openiam.mail.constants.MailTemplateParameters
import org.openiam.mq.util.UnwrapUtils
import org.openiam.provision.dto.ProvisionUser
import org.springframework.context.ApplicationContext

ApplicationContext appContext = (ApplicationContext) context

UserRabbitMQService userService = appContext.getBean(UserRabbitMQService.class)
ProvisionMQService provisionService = appContext.getBean(ProvisionMQService.class)
PasswordMQService passwordService = appContext.getBean(PasswordMQService.class)
MailRabbitMQService mailService = appContext.getBean(MailRabbitMQService.class)

// param1 = how many days ahead to activate. Blank -> 1.
int days = (param1 != null && !((String) param1).trim().isEmpty()) ? Integer.parseInt((String) param1) : 1

// The activation link base. Environment-specific, so it is a parameter, not a literal.
String activationUrl = (param2 != null && !((String) param2).trim().isEmpty())
        ? (String) param2
        : "https://<OPENIAM_HOST>/idp/activate"

Calendar from = Calendar.getInstance()
from.set(Calendar.HOUR_OF_DAY, 0); from.set(Calendar.MINUTE, 0)
from.set(Calendar.SECOND, 0);      from.set(Calendar.MILLISECOND, 0)

Calendar to = Calendar.getInstance()
to.add(Calendar.DAY_OF_MONTH, days)
to.set(Calendar.HOUR_OF_DAY, 23); to.set(Calendar.MINUTE, 59)
to.set(Calendar.SECOND, 59);      to.set(Calendar.MILLISECOND, 999)

UserSearchBean usb = new UserSearchBean()
usb.setStartDateToken(new DateSearchParam(from.getTime(), to.getTime(), MatchType.BETWEEN))

List<User> users = UnwrapUtils.unwrap(
        userService.findBeans(usb, [UserCollection.PRINCIPALS] as UserCollection[], 0, Integer.MAX_VALUE))

int activated = 0
int failed = 0

if (users != null) {
    for (User user : users) {
        // Only users still waiting on their start date are ours to touch.
        if (user.getStatus() != UserStatusEnum.PENDING_START_DATE) {
            continue
        }
        try {
            ProvisionUser pUser = new ProvisionUser(user)
            pUser.setStatus(UserStatusEnum.PENDING_INITIAL_LOGIN)
            provisionService.saveUser(pUser, false)

            Login login = user.getPrincipalByManagedSysId("0")
            if (login == null) {
                println "No primary identity for user ${user.id} - activation link not sent"
                failed++
                continue
            }

            PasswordResetTokenRequest tokenRequest =
                    new PasswordResetTokenRequest(login.getLogin(), login.getManagedSysId(), null)
            PasswordResetTokenResponse tokenResponse = passwordService.generatePasswordResetToken(tokenRequest)

            if (tokenResponse != null && tokenResponse.isSuccess() && tokenResponse.getPasswordResetToken() != null) {
                List<NotificationParam> msgParams = new LinkedList<NotificationParam>()
                msgParams.add(new NotificationParam(MailTemplateParameters.BASE_URL.value(), activationUrl))
                msgParams.add(new NotificationParam(MailTemplateParameters.USER_ID.value(), user.getId()))
                msgParams.add(new NotificationParam(MailTemplateParameters.TOKEN.value(), tokenResponse.getPasswordResetToken()))
                mailService.sendNotificationAsynchronously(user.getId(), msgParams, "NEW_USER_ACTIVATION_NOTIFICATION")
                activated++
            } else {
                println "Could not generate an activation token for user ${user.id}"
                failed++
            }
        } catch (Exception e) {
            println "Activation failed for user ${user.id}: ${e.message}"
            failed++
        }
    }
}

println "Activation run ${parentRequestId}: ${activated} activated, ${failed} failed, window ${from.getTime()} - ${to.getTime()}"

// 0 = success. Any other value marks the BATCH_TASK_EXECUTE audit record failed.
output = (failed == 0) ? 0 : 1
```

{% endcode %}

Four properties of that script worth copying:

* **`output = 0` only when the business result is right.** Not because the queue accepted a request. The `failed` counter is what drives the audit outcome.
* **Every service comes from `context.getBean(...)`.** No inherited field, no assumption about the binding map.
* **The environment-specific URL is a parameter, not a literal.** `param2` here; `<OPENIAM_HOST>` only as a fallback that is obviously wrong if it ever reaches production.
* **A per-user exception does not abort the run.** It increments `failed` and the loop continues, so one bad record does not leave the rest of the population half-processed.

`NEW_USER_ACTIVATION_NOTIFICATION` is a shipped mail template, caption *New User Activation*, and it reads `baseUrl` and `token` from the notification params — so those two parameter names are not arbitrary. Open it under the mail templates screen to see which parameters it expects. See [Shipped mail templates](/administration-guide/reference/shipped-mail-templates.md).

## Cron and overlap

The expression is a Spring cron expression, validated at save time only to the extent of catching one that cannot be parsed. It is evaluated in the **batch-task-manager service's** time zone, not the browser's. `0 30 4 * * ?` is 04:30, not 16:30. Record the first three expected fire times when you set it rather than reasoning about the fields, and check them against the **Scheduled Tasks** grid once the next sweep has run — that grid is where the service tells you what it actually intends to do.

There is no per-task concurrency guard. The service holds its lock only while it is *starting* work; the tasks themselves then run on their own threads. Set the interval longer than the worst-case runtime, or implement a lock inside the script. Two overlapping recalculation or synchronization runs contend for the same population and produce failures that look like data problems.

## Producing a scheduled report file

The second commonest reason to write a batch task, after running a synchronization, is to produce a file: a nightly CSV of who holds what on a managed system, or a one-off list of suspected duplicate records for somebody to confirm before anything is deleted. The product ships a working example of exactly this shape, and it is worth copying rather than starting from scratch.

### Where the file lands

Files a script writes through the file service go to one configured directory, and OpenIAM refuses any path that resolves outside it: the write fails with `File Access exception` rather than silently landing somewhere else. The directory is `iam.files.location`, shipped `${confpath}/upload/` — the same property as the CSV upload directory above, which is why the same file turns up under two different absolute paths on two installations of the same product. There is one property, not two conventions. Write into that directory or a subdirectory of it, and use the file service rather than opening a file yourself.

### The shipped example

`failedProvisionRequestReport_1.0.groovy` is a complete report task: it reads its scope from `param1`–`param3`, builds a CSV in memory, writes it through the file service, and mails it as an attachment. The parts to copy:

{% code overflow="wrap" lineNumbers="true" expandable="true" %}

```groovy
// 1. Parameters carry the scope. Nothing environment-specific is a literal.
String manSysIdsParam = (String) param1     // which managed systems
String daysParam      = (String) param2     // how far back
String emailsParam    = (String) param3     // who receives it

// 2. Record the scope on the audit event, so the run is self-documenting.
final AuditLogBuilder idmAuditLog = AuditLogHolder.getInstance().getEvent()
idmAuditLog.setAction(AuditAction.SEND_REPORT_OF_FAILED_PROVISION_REQUESTS)
idmAuditLog.setAuditDescription(String.format(
        "Parameters of task: managed systems by ids: %s, %s days ago to check, email addresses to notify: %s",
        manSysIdsParam, daysParam, emailsParam))

// 3. Build the CSV with a header row, quoting every value and stripping
//    newlines, quotes and tabs out of anything free-text.
String reportData = "USERNAME,DATE,MANAGED SYSTEM,ERROR 
"

// 4. Write it through the file service, not with new File(...).
FileRabbitMQService fileRabbitMQService = appContext.getBean(FileRabbitMQService.class)
fileRabbitMQService.saveFile(fileName, reportData + "
")

// 5. Record the row count on the audit event before you finish.
idmAuditLog.setAuditDescription("Number of found failed requests " + numberOfFailedRequests)
```

{% endcode %}

{% hint style="info" %}
The file name in that script is built with a date stamp — `String.format("failed_provision_requests_%s.csv", sdf.format(new Date()))`. Do the same. A fixed file name means each run overwrites the evidence of the last one, which is exactly what you need it for when somebody asks what the job did on Tuesday.
{% endhint %}

### Two-phase for anything destructive

A report task and a cleanup task are two tasks, not one with a flag. When the job is "find the duplicate or stale records and remove them", the order that survives review is this.

**Task one writes the list and changes nothing.** Same shape as the shipped example: identify the records, write the CSV, do not touch them. Schedule it far in the future or leave it unscheduled and drive it with **Execute** — that button runs a task once regardless of the Enabled flag.

**Somebody who owns the data confirms the list**, from the CSV rather than from a screen. This is the step that makes the extra task worth it: the list is a reviewable artefact with a timestamp in its name.

**Task two acts on the confirmed list only** — a second task reading the confirmed file or an explicit ID list in `param1`, not re-deriving the population. Re-deriving it means the set that gets deleted is not the set that was approved.

{% hint style="warning" %}
Set the report task's cron so that it cannot fire unattended before you have read its first output, and remember that the cron is evaluated in the batch-task-manager service's own time zone. `0 30 4 * * ?` is **04:30**, not 16:30 — see [Cron and overlap](#cron-and-overlap). Getting that wrong on a report task costs you a day; getting it wrong on the cleanup task costs you the records.
{% endhint %}

## Verification

| What you did                | Evidence                                                                                                                                                                                                         |
| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Saved                       | *Batch Task was saved successfully*; reopen the task and read `LAST_MODIFIED_DATETIME` back                                                                                                                      |
| Executed once               | A `BATCH_TASK_EXECUTE` audit record whose description is the **task name**, whose requestor is `org.openiam.idm.system.user.id`, and — for the script route — whose URL is `groovy://<GROOVY_SCRIPT_ID>`         |
| The script succeeded        | That same record is `SUCCESS` only if the script returned `0`. Any other integer, or a throw, marks it failed                                                                                                    |
| Scheduled                   | A row in the task's **Scheduled Tasks** grid, with the columns the screen shows: scheduled date, running, completed (`BATCH_SCHEDULE`)                                                                           |
| It actually ran on schedule | `LAST_EXEC_TIME` on the task advanced to the run's start time                                                                                                                                                    |
| The work happened           | The far end, never the audit line: the users' statuses, the target accounts, the mail queue, the pruned rows. See [Diagnose a failed operation](/administration-guide/operations/diagnose-a-failed-operation.md) |

For each task you enable, keep an owner, a purpose, the schedule with its time zone, the data scope, the expected duration, the overlap behaviour, the success evidence and the stop procedure. Do not enable a shipped row because it exists.

## Change, disable or roll back

{% stepper %}
{% step %}

### Capture the current definition first

Name, enabled flag, cron or run-on, script ID or bean.method, and all four parameters. There is no history on a `BATCH_CONFIG` row beyond `LAST_MODIFIED_DATETIME`.
{% endstep %}

{% step %}

### Turn **Is Active** off, then clear the pending schedule

Disabling stops the 5-minute sweep from creating new schedule rows, but an already-materialised row in the **Scheduled Tasks** grid will still be executed by the 20-second poll. Delete the pending rows from that grid if you need the task to stop now.
{% endstep %}

{% step %}

### Re-point, do not edit

Select the previous script version on the task, or restore the previous bean.method and parameters. Because the task stores a script **ID**, re-pointing is complete and immediate — see [Create a Groovy script](/administration-guide/automation/manage-groovy-scripts.md#roll-back).
{% endstep %}

{% step %}

### Run one bounded **Execute** to verify

Then re-enable.
{% endstep %}

{% step %}

### Decide whether the bad run must be remediated

Deleting the task reverses nothing. Read the `BATCH_TASK_EXECUTE` record and its children for what the failed run actually changed.
{% endstep %}
{% endstepper %}

## When it goes wrong

Two whole classes of failure have their own pages: every enabled task stopping at once is [Restart batch tasks that have all stopped](/administration-guide/automation/batch-tasks/diagnose-stalled-batch-tasks.md), and a scheduled feed that reports success while importing nothing is [Run a synchronization on a schedule](/administration-guide/automation/batch-tasks/schedule-a-synchronization.md#when-it-goes-wrong).

| Symptom                                                                                                                                                | Cause to check                                                                                                                                   | Fix                                                                                                            |
| ------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------- |
| Task never starts on its own                                                                                                                           | `ENABLED` is `N`, so the sweep skips it entirely                                                                                                 | Enable it, and allow one `org.openiam.batch.task.sweep.time` interval (5 min)                                  |
| Cron change had no effect for a few minutes                                                                                                            | The sweep interval is 300000 ms                                                                                                                  | Wait it out                                                                                                    |
| Disabled the task, it ran anyway                                                                                                                       | A `BATCH_SCHEDULE` row was already materialised; the 20-second timer runs it regardless                                                          | Delete the pending schedule rows from the task's grid                                                          |
| **Schedule** button missing                                                                                                                            | It renders only when the task is saved **and** enabled                                                                                           | Enable and save first; use **Execute** for a test run                                                          |
| **Execute** returns success, nothing happened                                                                                                          | The message is *successfully scheduled to run asynchronously* — the queue accepted it                                                            | Read the `BATCH_TASK_EXECUTE` audit record and the service log                                                 |
| Save rejected: *Groovy Script does not exist*                                                                                                          | `FILE_DOES_NOT_EXIST` — the script **ID** did not resolve                                                                                        | Re-select the script; after a restore or migration the stored ID may be stale or null                          |
| Save rejected: *Spring Bean is invalid…*                                                                                                               | `INVALID_SPRING_BEAN` — the pair is not in `executable-beans`, because it is not declared runnable from a batch task or lives in another service | Pick from the dropdown; a bean in the workflow or ESB service cannot be scheduled from here                    |
| Bean task runs but does nothing, `LAST_EXEC_TIME` never advances                                                                                       | No method matched: a single-string method with `PARAM1` empty, or a no-argument method with `PARAM1` set — and the mismatch is skipped silently  | Match `PARAM1`'s presence to the method's arity                                                                |
| `PARAM2`–`PARAM4` ignored                                                                                                                              | The bean route passes `param1` only                                                                                                              | Use the Groovy route if you need more than one parameter                                                       |
| Log shows `Can't invoke spring thread`                                                                                                                 | The bean could not be resolved or the invocation threw — including a bean that is not in the batch-task-manager service                          | Confirm the bean exists in *this* service; see the `TASK_EMAIL_NOTIFICATION` note above                        |
| Log shows `Cannot execute batch groovy task`                                                                                                           | The script threw, or returned something that is not an `Integer`                                                                                 | End the script with `output = 0`; do not return a String or leave a trailing statement of another type         |
| Audit record marked failed although the work succeeded                                                                                                 | The script returned a non-zero integer                                                                                                           | Return `0` for success                                                                                         |
| Task fails only after an upgrade                                                                                                                       | The script's imports or a service signature changed, or a stored script ID was nulled (look for `Could not find groovy script:` in the log)      | Run **Compile All Groovy Scripts**; re-select the script                                                       |
| Duplicate effects                                                                                                                                      | Overlapping schedules, or a manual **Execute** during a scheduled run — there is no per-task guard                                               | Lengthen the interval or lock inside the script                                                                |
| Synchronization "already running"                                                                                                                      | The sync's own overlap guard                                                                                                                     | Do not force a second run; read the synchronization history                                                    |
| Task runs on the wrong clock                                                                                                                           | Cron is evaluated in the batch-task-manager service's time zone                                                                                  | Establish that service's zone with whoever owns the deployment; do not assume the browser's                    |
| Approvals never chased                                                                                                                                 | `REQUEST_REMINDERS` and `REQUEST_ESCALATION` ship disabled                                                                                       | Enable both; see [Approval flows](/administration-guide/requests/approval-flows.md#debugging-a-stuck-request)  |
| Audit table growing without limit                                                                                                                      | `PRUNE_AUDIT_LOGS` ships disabled **and** every `org.openiam.audit.log.prune.days.*` ships `-1`                                                  | Change both; see [Audit actions and retention](/administration-guide/reference/audit-actions-and-retention.md) |
| A report script fails with `File Access exception`                                                                                                     | The path resolved outside `iam.files.location`, and OpenIAM refuses it rather than writing elsewhere                                             | Write into `${confpath}/upload/` or a subdirectory of it                                                       |
| A user search from a script fails with `Parameter value [...] did not match expected type [org.openiam.idm.srvc.meta.domain.MetadataTypeEntity (n/a)]` | The search bean carries employee type as a string; OpenIAM stores it as a metadata type reference, so the database path cannot bind it           | Set `useElasticSearch` true on the search bean, or filter the type in the script                               |
| A script's error surfaces only as `Exception Occurred during RabbitMQ Call`                                                                            | The generic MQ wrapper; the real cause is the line beneath it                                                                                    | Read the exception under it, not the wrapper line                                                              |

## Related

* [Shipped batch tasks](/administration-guide/reference/shipped-batch-tasks.md) — all fifteen named tasks with cron, bean and shipped enabled state, plus the thirteen executable beans
* [Restart batch tasks that have all stopped](/administration-guide/automation/batch-tasks/diagnose-stalled-batch-tasks.md) — the schedule table, and the one task's backlog that stops every task
* [Run a synchronization on a schedule](/administration-guide/automation/batch-tasks/schedule-a-synchronization.md) — the commonest real use of a batch task
* [Create a Groovy script](/administration-guide/automation/manage-groovy-scripts.md) — creating, versioning and rolling back the script a task runs
* [Diagnose a failed operation](/administration-guide/operations/diagnose-a-failed-operation.md) — reading the batch-task-manager log and the audit trail
* [Audit actions and retention](/administration-guide/reference/audit-actions-and-retention.md) — `BATCH_TASK_EXECUTE` and the `-1` retention defaults
* [Installation Guide](/installation/installation.md) — the upload directory's absolute path per installation type, service time zones, and bulk database cleanup

*Applies to OpenIAM 2026.8.1. Last reviewed 28 August 2026.*


---

# 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/administration-guide/automation/batch-tasks.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.
