# OKMFlow Workflow Engine 1.8

> OKMFlow is a workflow engine integrated with OpenKM (document management system). It allows you to model and execute business processes using nodes and transitions. Scripts are written in Groovy and have access to the workflow context and the OpenKM API.

## Process Modelling

A process is composed of **nodes** and **transitions**.

- **Process definition** (`ProcessDefinition`): template that describes the structure of the workflow.
- **Process instance** (`ProcessInstance`): a concrete execution of a process definition.

### Node types

| Type        | Description |
|-------------|-------------|
| Start       | Entry point. Each process must have exactly one. |
| End         | Termination point. A process may have several. |
| Task        | Requires human interaction. Pauses the workflow until the user completes the task. |
| Decision    | Automatic branching via script. Returns the name of the transition to follow. |
| Mail        | Sends an email automatically. |
| Action      | Executes Groovy code in response to process events. |
| Scheduled   | Runs periodically via cron. Name must use the prefix `scheduled_`. |
| Library     | Defines reusable global methods and variables. A special Action node. |

### Transitions

Connect nodes. They have a source node (`from`) and a target node (`to`). The name is optional when there is only one outgoing transition, but **mandatory and unique** when there are several (the source node script returns the name of the transition to follow).

---

## Start Node

The Start node defines the entry point of the workflow. Each process must have exactly one.

**Properties:**
- `Source position`: visual position from which the transition leaves (Top/Bottom/Left/Right).
- `Id`: unique identifier, automatically generated.

**Constraints:** only one outgoing transition allowed. Does not accept incoming transitions.

### Context variables initialised by the Start Node

| Variable           | Type                                | Description |
|--------------------|-------------------------------------|-------------|
| `processInstance`  | `ProcessInstance`                   | The running process instance. Use `context.get("processInstance").getId()` to obtain the process instance ID inside scripts and `assignExpr`. |
| `initiator`        | `Actor`                             | User who started the workflow. Retrieved with `context.get("initiator")`, the same lookup pattern used for every context variable. |
| `processDefinition`| `ProcessDefinition`                 | The process template. |
| `uuid`             | `String`                            | UUID of the associated OpenKM node (only when the workflow is started from an OpenKM node). |
| `node`             | `Document / Folder / Mail / Record` | Associated OpenKM object (only when `uuid` exists). |

> **Important:** `uuid` and `node` are only available when the workflow is started from an OpenKM node. If started manually or programmatically without an associated node, these variables will not be present.

---

## End Node

The End node defines a termination point of the process. A workflow may have multiple End nodes to represent different outcomes (e.g. "Approved", "Rejected", "Cancelled").

**Properties:**
- `Name`: descriptive name. Mandatory and unique when there are multiple End nodes.
- `Target position`: visual position where transitions arrive.
- `Id`: unique identifier, automatically generated.

**Behaviour:**
- When an End node is reached, the process instance terminates immediately.
- Does not initialise or modify context variables.
- Only accepts incoming transitions; has no outgoing transitions.

> **Important:** when a workflow contains multiple End nodes, each must have a different name.

---

## Task Node

The Task node represents work that requires human interaction. When the workflow reaches a Task node, it pauses and waits for the user to complete the task via the graphical interface.

### Main properties (Task tab)

| Property           | Type        | Description |
|--------------------|-------------|-------------|
| `Name`             | Text        | Descriptive name of the task (e.g. "Approve invoice", "Review document"). |
| `Id`               | Number      | Unique identifier, automatically generated. |
| `Due date`         | Days/Hours  | Expiry period relative to the moment of assignment. Generates ISO 8601 format (e.g. `P2DT3H`). |
| `Repeat`           | Days/Hours  | Interval for resending overdue task notifications. |
| `Source position`  | Dropdown    | Position of outgoing transitions (Top/Bottom/Left/Right). |
| `Target position`  | Dropdown    | Position of incoming transitions (Top/Bottom/Left/Right). |
| `Description`      | Textarea    | Optional documentation of the task's purpose. |
| `Form definition`  | XML Editor  | XML form that the user fills in when completing the task. |
| `Assign expression`| Script      | Groovy script that returns the assigned user or list of users. |

> **Important:** when generating `.okmflow` JSON files, `dueDate` and `repeat` must always be set to `""` (empty string), never `null`. Setting them to `null` causes a JavaScript error in the workflow editor UI: `can't access property "replace", durationString is null`.

> **Important:** `form` must never be `null` for a Task node, even in an early skeleton pass where forms will be filled in later. A `null` form causes `ParseException caused by Missing XML workflow form definition` at import time. A minimal placeholder is enough to satisfy the parser — a `<workflow-form>` with at least one field and one `<button>` per outgoing transition name.

### Notification properties (Task notification tab)

| Property  | Type             | Description |
|-----------|------------------|-------------|
| `Notify`  | Toggle           | Enables sending an email when the task is assigned. |
| `Subject` | Text             | Email notification subject. |
| `Body`    | Rich Text Editor | Email notification body. |

### Assign expression — return type rules

The return type determines the assignment mode:

- **Return `String`** → **direct assignment** to a single user. The task is immediately assigned and visible only to that user. Use this when you know exactly who should handle the task (including re-assigning to the same user who handled a previous task).
- **Return `List<String>`** → **pooled task**. The task appears in all listed users' inboxes and any one of them must self-assign it before working on it. Even a list with one element behaves as a pool — the user must explicitly claim the task.

> **Critical:** if you want to reassign a task to the same actor who handled a prior task (e.g. re-routing after clarification), you **must return a `String`**, not a `List` with one element. A single-element list still requires the user to self-assign.

### Assign expression — examples

**Direct assignment to a user:**
```groovy
return "okmAdmin";
```

**Pool task (any user in the group can claim it):**
```groovy
import java.util.*;

List users = new ArrayList();
users.add("okmAdmin");
users.add("joe");
return users;
```

**Assign to the workflow initiator:**
```groovy
import com.openkm.sdk4j.impl.OKMWebservices;
import com.openkm.sdk4j.bean.*;
import com.openkm.okmflow.util.*;
import com.openkm.okmflow.bean.*;
import com.openkm.bean.form.*;
import com.openkm.util.*;

Class baseLibrary = ScriptUtils.evaluateFromNode("library_global_base");
def initiatorId = baseLibrary.getInitiatorId(context);
return initiatorId;
```

**Assign to all users with a specific role:**
```groovy
import com.openkm.sdk4j.impl.OKMWebservices;
import com.openkm.sdk4j.bean.*;
import com.openkm.okmflow.util.*;
import com.openkm.okmflow.bean.*;
import com.openkm.bean.form.*;
import com.openkm.util.*;
import java.util.*;

OKMWebservices ws = WebservicesHelper.getInstance();
List<String> users = new ArrayList();
for (CommonUser commonUser : ws.auth.getUsersByRole("ROLE_ADMIN")) {
  users.add(commonUser.getId());
}
return users;
```

### Form variables and context

When the user submits the form, the values are stored in the context using the `name` attribute of each field:

```xml
<input label="Invoice number" name="invoiceNumber" />
```

Creates the context variable `invoiceNumber` holding an `Input` object with the entered value.

> **Important:** if a context variable already exists with the same name as a form field, it will be overwritten when the form is submitted. This applies to **every field type** (`input`, `select`, `textarea`, `upload`, etc.), not just a specific one — the engine always saves the submission result under the field's `name` in context.

> **Naming convention — prevent overwrite:** if a value was pre-loaded in an earlier Action node under context key `"someKey"` (via `context.put("someKey", ...)` or a field's `data="someKey"`), and a form field's `name` is also `"someKey"`, the pre-loaded value will be silently overwritten on submission. This is especially destructive for pre-loaded descriptor objects such as `Upload` (e.g. losing a `folderUuid` set before the task). To preserve the original value for re-use in later tasks, use a **different** value for `data` than for `name` (convention: suffix the context key with `Data`, e.g. `name="budgetDocument"`, `data="budgetDocumentData"`). See the Task Node form field reference for the full `<upload>` example.

> **Warning — use `def` instead of typed casts when the field type may vary.** Casting to a specific form class (`Input`, `TextArea`, `Select`, etc.) causes a `ClassCastException` if the actual stored type differs. Use Groovy's `def` keyword to read form variables safely, especially when passing them through to another context key without inspecting the value:
>
> ```groovy
> // WRONG — throws ClassCastException if manager_comment was defined as TextArea
> Input comment = context.get("manager_comment");
>
> // CORRECT — type-neutral; works for Input, TextArea, Select, etc.
> def comment = context.get("manager_comment");
> context.put("reviewer_comment", comment); // no new object needed — just re-map
> ```
>
> Only cast to a specific type when you are certain of the field type **and** need to call type-specific methods (e.g. `Select.getValue()`). For simply moving a value from one context key to another, always use `def`.

---

## Decision Node

The Decision node allows automatic branching based on programmatic logic. When the workflow reaches a Decision node, it executes a script that evaluates conditions and returns the name of the outgoing transition to follow.

**Properties:**
- `Name`: descriptive name of the decision (e.g. "Check if second recipient exists").
- `Id`: unique identifier, automatically generated.
- `Source position` / `Target position`: visual position (cannot be the same value).
- `Description`: optional documentation.
- `Script definition`: Groovy script that must return the exact name of an outgoing transition.

**Transition requirements:**
- At least two outgoing transitions, each with a unique name.
- The value returned by the script must exactly match one of those names.

### Script example

```groovy
import com.openkm.sdk4j.impl.OKMWebservices;
import com.openkm.sdk4j.bean.*;
import com.openkm.sdk4j.util.*;
import com.openkm.okmflow.util.*;
import com.openkm.okmflow.bean.*;
import com.openkm.bean.form.*;
import com.openkm.util.*;
import java.util.*;
import org.apache.commons.lang3.StringUtils;

OKMWebservices ws = WebservicesHelper.getInstance();
Class baseLibrary = ScriptUtils.evaluateFromNode("library_global_base");
def secondRecipient = baseLibrary.getContextValue(context, "secondRecipient");

if (StringUtils.isNotBlank(secondRecipient.getValue())) {
    return "yes";
} else {
    return "no";
}
```

The Decision node must have two outgoing transitions named `"yes"` and `"no"`.

> **Important:** if the script returns a value that does not match any outgoing transition, the workflow execution will fail.

---

## Action Node

The Action node executes custom logic automatically, without pausing the workflow. It is used to manage context variables, call the OpenKM API, or execute any business logic.

**Properties:**
- `Name`: descriptive name of the action.
- `Id`: unique identifier, automatically generated.
- `Source position` / `Target position`: visual position (cannot be the same value).
- `Description`: optional documentation.
- `Script definition`: Groovy script executed when the node is reached.

> **Important:** `script` must never be `null` for an Action node, even in an early skeleton pass where scripts will be filled in later. A `null` script causes `ParseException caused by Script text to compile cannot be null!` at import time. An empty string (`""`) is accepted as a valid placeholder — unlike Task `form` (above), a transition's own `Script definition` (see Transitions section) does **not** need this treatment and may safely stay `null`.

### Optional routing capability

If the script returns a `String`, the Action node also acts as a Decision node: the returned value is used to select the outgoing transition. If it returns nothing, execution simply continues to the next node.

### Multiple incoming transitions (fan-in)

An Action node may be the target of transitions coming from **two or more different Task nodes**, not just from the same task or a loop back into it. This works without any special configuration, provided every calling task's form uses identical field `name`s for any values the shared Action reads from context (e.g. two different task forms both having a `managerComments` textarea, so the shared Action can safely do `context.get("managerComments")` regardless of which task actually triggered it) — the Action itself has no way to know which task triggered it, so it cannot adapt per-caller.

### Common uses

- Manage context variables (create, update, delete).
- Add or read document metadata via the OpenKM API.
- Perform calculations and store results in the context.
- Call external APIs.
- Document operations (create, move, copy).

### Script example

```groovy
import com.openkm.sdk4j.impl.OKMWebservices;
import com.openkm.sdk4j.bean.*;
import com.openkm.okmflow.util.*;
import com.openkm.okmflow.bean.*;
import com.openkm.bean.form.*;
import com.openkm.util.*;
import java.util.*;

OKMWebservices ws = WebservicesHelper.getInstance();
Class baseLibrary = ScriptUtils.evaluateFromNode("library_global_base");
def invoiceNumber = baseLibrary.getContextValue(context, "invoice_number");
def total = baseLibrary.getContextValue(context, "total");
def uuid = baseLibrary.getContextValue(context, "uuid");

Map<String,String> properties = new HashMap();
properties.put("okp:invoice.number", invoiceNumber.getValue());
properties.put("okp:invoice.total", total.getValue());
ws.propertyGroup.setProperties(uuid, "okg:invoice", properties);

// Optional routing (acts as a Decision node)
int totalValue = Integer.parseInt(total.getValue());
if (totalValue > 1000) {
  return "supervisor";
} else {
  return "go";
}
```

---

## Mail Node

The Mail node sends email notifications to one or more recipients as part of the process. It executes automatically without pausing the workflow.

**Properties:**

| Property           | Type        | Description |
|--------------------|-------------|-------------|
| `Name`             | Text        | Unique node name. |
| `Source position`  | Dropdown    | Position of incoming transitions. |
| `Target position`  | Dropdown    | Position of outgoing transitions. Cannot match Source. |
| `Description`      | Textarea    | Optional documentation. |
| `Recipients`       | Textarea    | Comma-separated email addresses. Supports context variables (e.g. `${emails}`). |
| `Subject`          | Text        | Email subject. Supports context variables. |
| `Body`             | HTML Editor | Email body in HTML. Supports context variables and FreeMarker expressions. |

**The Mail node does not support attachments.** If the outgoing email needs a document attached, send it from an Action node instead, using the mail-sending methods provided by the Java SDK that accept attachments.

### Context variables in the Mail node

Used with the `${variable}` syntax:

- `${initiator.email}` — email of the workflow initiator.
- `${initiator.name}` — full name of the initiator.
- `${node.path}` — path of the document associated with the workflow.
- `${uuid}` — UUID of the associated document.
- Custom variables created by forms or Action nodes (e.g. `${emails}`, `${subject}`).

### Recipient separators

- In the `Recipients` field (manual entry): separated by **comma** (`,`).
- In context variables used as `${emails}`: separated by **semicolon** (`;`).

### FreeMarker support in the body

```
<#if message.value?? && message.value != "">
<strong>Extra message:</strong>
${message.value}
<#else>
No message available
</#if>
```

### Preparing recipients from an Action node

Common pattern when recipients are selected in a form (Select field with user IDs):

```groovy
import com.openkm.sdk4j.impl.OKMWebservices;
import com.openkm.sdk4j.bean.*;
import com.openkm.okmflow.util.*;
import com.openkm.okmflow.bean.*;
import com.openkm.bean.form.*;
import com.openkm.util.*;
import java.util.*;

OKMWebservices ws = WebservicesHelper.getInstance();

Select users = (Select) context.get("users");
String usersIdSemicolonSeparated = users.getValue();

List<String> userList = WorkflowUtils.convertToListFromSelectValue(usersIdSemicolonSeparated);
List<String> emailList = new ArrayList();
for (String userId : userList) {
  CommonUser commonUser = ws.auth.getUser(userId);
  emailList.add(commonUser.getEmail());
}
String emails = String.join(";", emailList); // Mail node requires semicolon

context.put("emails", emails);
```

---

## Library Node

The Library node is a special Action node that defines reusable Groovy code (classes with methods and constants). It is not part of the workflow execution flow; other nodes load it via `ScriptUtils.evaluateFromNode("library_name")`.

**Naming convention:** use the prefix `library_` (e.g. `library_global_base`, `library_invoice_utils`).

**Requirement:** the script must end with a `return` that returns the defined class.

### How to load and use a library

```groovy
import com.openkm.okmflow.util.*;

Class baseLibrary = ScriptUtils.evaluateFromNode("library_global_base");
def initiatorId = baseLibrary.getInitiatorId(context);
```

> **Recommendation:** when starting a new workflow project, always consider loading `library_global_base` from the shared `Global library.okmflow`. It provides logging, context access, JSON serialisation, and other utilities that would otherwise need to be re-implemented in every workflow.

### `library_global_base` — available methods

| Method | Description |
|--------|-------------|
| `BaseLibrary.logInfo(component, message)` | Logs an info message via `FileLogger`. |
| `BaseLibrary.logError(component, message, e)` | Logs an error + stack trace. `e` is optional. |
| `BaseLibrary.getWebservices()` | Returns a configured `OKMWebservices` instance. |
| `BaseLibrary.getContextValue(context, key)` | Reads a value from the workflow context. |
| `BaseLibrary.setContextValue(context, key, value)` | Writes a value to the workflow context. |
| `BaseLibrary.getInitiatorId(context)` | Returns the user ID of the workflow initiator. |
| `BaseLibrary.getProcessInstanceId(context)` | Returns the process instance ID (`long`). |
| `BaseLibrary.convertToJson(obj)` | Serialises any object to a JSON string (Jackson). |
| `BaseLibrary.convertFromJson(json, Class)` | Deserialises a JSON string to the given class. |
| `BaseLibrary.hasDurationElapsed(duration, startTime)` | Returns `true` if elapsed time ≥ ISO 8601 duration (e.g. `"PT2H"`). Used in Scheduled nodes. |

Full source and detailed documentation in `llms-full-okmflow-samples.txt` → *Global Library* section.

### Example: base library (`library_global_base`)

```groovy
import com.openkm.sdk4j.impl.OKMWebservices;
import com.openkm.sdk4j.bean.*;
import com.openkm.okmflow.util.*;
import com.openkm.okmflow.bean.*;
import com.openkm.util.*;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import java.time.LocalDateTime;
import java.time.Duration;

class BaseLibrary {

    static void logInfo(String component, String message) {
        FileLogger.info(component, message);
    }

    static void logError(String component, String message, Exception e = null) {
        FileLogger.error(component, message + "\n" + StackTraceUtils.toString(e));
    }

    static OKMWebservices getWebservices() {
        return WebservicesHelper.getInstance();
    }

    static Object getContextValue(Map<String,Object> context, String key) {
        return context.get(key);
    }

    static void setContextValue(Map<String,Object> context, String key, Object value) {
        context.put(key, value);
    }

    static String getInitiatorId(Map<String,Object> context) {
        Actor actor = context.get("initiator");
        return actor.getId();
    }

    static long getProcessInstanceId(Map<String,Object> context) {
        def procIns = context.get("processInstance");
        return procIns.getId();
    }

    static String convertToJson(Object obj) {
        ObjectMapper objectMapper = new ObjectMapper();
        return objectMapper.writeValueAsString(obj);
    }

    static <T> T convertFromJson(String jsonString, Class<T> targetClass) {
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        return objectMapper.readValue(jsonString, targetClass);
    }

    static boolean hasDurationElapsed(String durationString, LocalDateTime startTime) {
        Duration expectedDuration = Duration.parse(durationString);
        LocalDateTime currentTime = LocalDateTime.now();
        Duration elapsedDuration = Duration.between(startTime, currentTime);
        return elapsedDuration.compareTo(expectedDuration) >= 0;
    }
}

return BaseLibrary
```

### Example: constants library

```groovy
class Constants {
    static final String CONTEXT_UUID = "uuid"
    static final String CONTEXT_INITIATOR = "initiator"
    static final String STATUS_PENDING = "PENDING"
    static final String STATUS_APPROVED = "APPROVED"
    static final String STATUS_REJECTED = "REJECTED"
}

return Constants
```

### Example: business rules library

```groovy
class BusinessRules {
    static boolean requiresManagerApproval(double amount) {
        return amount > 1000.0
    }

    static boolean requiresDirectorApproval(double amount) {
        return amount > 10000.0
    }

    static String determineApprovalLevel(double amount) {
        if (requiresDirectorApproval(amount)) {
            return "DIRECTOR"
        } else if (requiresManagerApproval(amount)) {
            return "MANAGER"
        } else {
            return "SUPERVISOR"
        }
    }
}

return BusinessRules
```

---

## Scheduled Node

The Scheduled node is a special Action node that runs periodically via a system cron. Unlike regular Action nodes that run once, Scheduled nodes run repeatedly until a condition is met or the workflow ends.

**Identification:** the node name must start with the prefix `scheduled_` (e.g. `scheduled_check_approvals`, `scheduled_wait_for_child_workflows`).

**Execution interval:** configurable in `workflow.properties`:
```properties
scheduled.actions.rate=PT60S
```
ISO 8601 format. Default value is 60 seconds. Requires a service restart to apply changes.

**Positions:** not applicable (Source/Target position). The node is not connected to the diagram flow.

### Main use case: child workflow synchronisation

Common pattern when a parent workflow launches several child workflows in parallel and must wait for all of them to finish.

**Flow:**
1. The parent launches N child workflows and stores their count and list in the context.
2. The parent waits at an unassigned Task ("Waiting Task").
3. Each child workflow, when it ends, notifies the parent by adding a status variable to the parent's context.
4. The parent's Scheduled node periodically checks whether all children have finished.
5. When all have responded, the Scheduled node programmatically completes the "Waiting Task".
6. The parent resumes execution.

**Example 1 — Parent Action node: launch child workflows in parallel**

```groovy
import com.openkm.sdk4j.impl.OKMWebservices;
import com.openkm.sdk4j.bean.*;
import com.openkm.okmflow.util.*;
import com.openkm.okmflow.bean.*;
import com.openkm.util.*;

Class Constants = ScriptUtils.evaluateFromNode("library_request_contants");
Class baseLibrary = ScriptUtils.evaluateFromNode("library_global_base");

def processDefinition = WorkflowUtils.getProcessDefinitionByName("Manager-voting");
long managerPdId = processDefinition.getId();

def uuid = baseLibrary.getContextValue(context, Constants.CONTEXT_UUID);
def initiator = baseLibrary.getInitiatorId(context);
def managerSelect = baseLibrary.getContextValue(context, Constants.CONTEXT_MANAGERS);
def managerList = managerSelect.value.tokenize(";");
long processInstanceId = baseLibrary.getProcessInstanceId(context);

baseLibrary.setContextValue(context, Constants.CONTEXT_MANAGERS_LIST, managerList);

def actorNum = 0;
managerList.each { manager ->
  actorNum++;
  def props = [uuid: uuid, initiator: initiator, actor: manager, srcProcInsId: processInstanceId];
  def result = WorkflowUtils.runProcessDefinition(managerPdId, props);
  context.put("pi_" + manager, result.get());
}

baseLibrary.setContextValue(context, Constants.CONTEXT_ACTOR_NUM, actorNum);
```

**Example 2 — Child Action node: notify parent when done**

```groovy
import com.openkm.sdk4j.impl.OKMWebservices;
import com.openkm.sdk4j.bean.*;
import com.openkm.okmflow.util.*;
import com.openkm.okmflow.bean.*;
import com.openkm.util.*;

Class Constants = ScriptUtils.evaluateFromNode("library_request_contants");
Class baseLibrary = ScriptUtils.evaluateFromNode("library_global_base");

def piId = baseLibrary.getContextValue(context, Constants.CONTEXT_SOURCE_PROCESS_INSTANCE_ID);
def actor = baseLibrary.getContextValue(context, Constants.CONTEXT_ACTOR);
def wfVar = Constants.VARIABLE_STATUS_PREFIX + actor;
WorkflowUtils.addProcessInstanceVariable(piId, wfVar, Constants.STATUS_ENDED);
```

**Example 3 — Parent Scheduled node: check whether all children have finished**

```groovy
import com.openkm.sdk4j.impl.OKMWebservices;
import com.openkm.sdk4j.bean.*;
import com.openkm.okmflow.util.*;
import com.openkm.okmflow.bean.*;
import com.openkm.util.*;

Class Constants = ScriptUtils.evaluateFromNode("library_request_contants");
Class baseLibrary = ScriptUtils.evaluateFromNode("library_global_base");

long processInstanceId = baseLibrary.getProcessInstanceId(context);
def managerList = baseLibrary.getContextValue(context, Constants.CONTEXT_MANAGERS_LIST);
def actorNum = baseLibrary.getContextValue(context, Constants.CONTEXT_ACTOR_NUM);
def uuid = baseLibrary.getContextValue(context, Constants.CONTEXT_UUID);
def props = [uuid: uuid];
def actorResp = 0;

String waitingTask = "Waiting Task";
def taskInstance = WorkflowUtils.getCurrentTaskInstance(processInstanceId);
if (waitingTask.equals(taskInstance.getName())) {
  if (managerList != null) {
    managerList.each { manager ->
      def wfVar = Constants.VARIABLE_STATUS_PREFIX + manager;
      if (context.get(wfVar) != null) {
        actorResp++;
      }
    }
  }

  if (actorResp == actorNum) {
    WorkflowUtils.setTaskInstanceValues(processInstanceId, waitingTask, null, props);
  }
}
```

---

## Initiation Form

The initiation form is a special Task node with the reserved name `run_config`. When the workflow engine detects this name, it presents the form to the user before starting workflow execution, allowing initial input data to be collected.

**The initiation form is optional.** If the workflow does not require prior data, simply do not include any task named `run_config`.

### Relevant properties

| Property           | Usage        | Description |
|--------------------|--------------|-------------|
| `Name`             | Mandatory    | Must be exactly `run_config`. |
| `Form definition`  | Main use     | Defines the XML fields presented to the user. |
| `Assign expression`| Required empty | Must contain `return "";` to pass system validation. |
| `Due date`         | Not applicable | Not used in the initiation form. |
| `Repeat`           | Not applicable | Not used in the initiation form. |
| `Source/Target position` | Not applicable | The initiation form is not connected to the diagram. |

### Behaviour

1. The user starts the workflow.
2. Before execution begins, the `run_config` form is presented.
3. The user fills in the fields and submits.
4. The values are stored in the workflow context.
5. The workflow begins execution at the Start node with the context already populated.

### Initiation form example

```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE workflow-form PUBLIC "-//OpenKM//DTD Workflow Form 1.0//EN"
                                "https://www.openkm.com/dtd/workflow-form-1.0.dtd">
<workflow-form>
  <input label="Requester name" name="requesterName" />
  <input label="Department"     name="department" />
  <textarea label="Purchase description" name="description" />
  <input label="Estimated amount" name="amount" />
  <select label="Priority" name="priority">
    <option label="Low"    value="low" />
    <option label="Medium" value="medium" />
    <option label="High"   value="high" />
  </select>
</workflow-form>
```

This form creates the context variables: `requesterName`, `department`, `description`, `amount`, `priority`.

### Assign expression

```groovy
return "";
```

---

## Transitions

Transitions connect nodes and define the execution flow of the workflow.

### Properties

| Property           | Type         | Description |
|--------------------|--------------|-------------|
| `Name`             | Text         | Optional when there is only one outgoing transition. **Mandatory and unique** when there are several. |
| `Type`             | Dropdown     | Visual style: Default, Step, Smooth Step, Straight. Affects only the visual appearance. |
| `Animated`         | Checkbox     | Makes the line blink in the diagram. Useful to highlight important paths. |
| `Color`            | Color Picker | Line colour. Common convention: green=approval, red=rejection, blue=standard flow, yellow=conditional. |
| `Script definition`| Code Editor  | Optional Groovy script executed when the transition is traversed. **Must not return a value.** |

> **Note:** unlike a Task's `form` or an Action's `script` (which must never be `null` — see Task Node / Action Node above), a transition's `Script definition` may safely stay `null`. Only named or conditional transitions typically need real Groovy; there is no parser requirement to fill unused ones with a placeholder.

### Naming rules

- **Single outgoing transition:** the name is recommended but not mandatory.
- **Multiple outgoing transitions:** each transition must have a unique name. The source node script returns the exact name of the transition to follow (case-sensitive).

```groovy
// Example in a Decision or Action node with multiple outputs
if (approvalRequired) {
    return "approved";
} else {
    return "rejected";
}
```

### Scripts on transitions

Transition scripts have access to the same context as Action nodes, but **must not return a value** to control routing (the path is already determined at this point).

Appropriate use: operations specific to a particular path (e.g. updating metadata when taking the "approved" transition).

```groovy
import com.openkm.sdk4j.impl.OKMWebservices;
import com.openkm.sdk4j.bean.*;
import com.openkm.okmflow.util.*;
import com.openkm.okmflow.bean.*;
import com.openkm.bean.form.*;
import com.openkm.util.*;
import java.util.*;

OKMWebservices ws = WebservicesHelper.getInstance();
Class baseLibrary = ScriptUtils.evaluateFromNode("library_global_base");
def invoiceNumber = baseLibrary.getContextValue(context, "invoice_number");
def total = baseLibrary.getContextValue(context, "total");
def uuid = baseLibrary.getContextValue(context, "uuid");

Map<String,String> properties = new HashMap();
properties.put("okp:invoice.number", invoiceNumber.getValue());
properties.put("okp:invoice.total", total.getValue());
ws.propertyGroup.setProperties(uuid, "okg:invoice", properties);
```

### When to use a transition script vs an Action node

| Use case                                   | Recommendation         |
|--------------------------------------------|------------------------|
| Conditional routing logic                  | Action node or Decision node |
| Complex business logic                     | Action node            |
| Simple operation specific to one path      | Transition script      |
| Update metadata on approval                | Transition script      |

---

## Recommended default imports for Groovy scripts

Include this block at the top of **every** script (Action, Decision, Transition, `assignExpr`). It covers virtually all use cases without causing conflicts:

```groovy
import com.openkm.sdk4j.impl.OKMWebservices;
import com.openkm.sdk4j.bean.*;
import com.openkm.okmflow.util.*;
import com.openkm.okmflow.bean.*;
import com.openkm.bean.form.*;
import com.openkm.util.*;
```

Add the following imports only when specifically needed:

```groovy
import com.openkm.sdk4j.util.*;           // SDK utilities (less common)
import java.util.*;                        // Java collections (ArrayList, HashMap, etc.)
import org.apache.commons.lang3.StringUtils; // StringUtils.isNotBlank() etc.
```

## Key utilities

| Utility                                              | Description |
|------------------------------------------------------|-------------|
| `WebservicesHelper.getInstance()`                    | Gets the OpenKM API client. |
| `ScriptUtils.evaluateFromNode("library_name")`       | Loads a Library node by name. |
| `WorkflowUtils.getProcessDefinitionByName("name")`   | Gets a process definition by name. |
| `WorkflowUtils.runProcessDefinition(pdId, props)`    | Launches a new process instance. |
| `WorkflowUtils.getCurrentTaskInstance(piId)`         | Gets the current task of a process instance. |
| `WorkflowUtils.setTaskInstanceValues(piId, taskName, transition, props)` | Programmatically completes a task. |
| `WorkflowUtils.addProcessInstanceVariable(piId, key, value)` | Adds a variable to another process instance's context. |
| `WorkflowUtils.convertToListFromSelectValue(value)`  | Converts a Select field value (`;`-separated) to `List<String>`. |
| `FileLogger.info(component, message)`                | Writes an informational message to the log. |
| `FileLogger.error(component, message)`               | Writes an error message to the log. |

---

## Workflow Beans

DTOs and context objects used in workflow Groovy scripts.

### Classes available in the execution context

These classes appear directly in the workflow `context` (injected by the engine).

#### Actor (`com.openkm.okmflow.bean.Actor`)

Represents the user who started the workflow. Available in the context as `initiator`.

| Property | Type           | Description |
|----------|----------------|-------------|
| `id`     | `String`       | Unique user identifier. |
| `name`   | `String`       | Display name of the user. |
| `email`  | `String`       | Email address. |
| `roles`  | `List<String>` | List of roles assigned to the user. |

```groovy
Actor actor = context.get("initiator");
String userId = actor.getId();
String email  = actor.getEmail();
```

#### ProcessDefinition (`com.openkm.okmflow.bean.ProcessDefinition`)

Process template. Available in the context as `processDefinition`.

| Property  | Type      | Description |
|-----------|-----------|-------------|
| `id`      | `Long`    | Unique identifier of the definition. |
| `name`    | `String`  | Process name. |
| `version` | `Integer` | Definition version. |

```groovy
ProcessDefinition pd = context.get("processDefinition");
String processName = pd.getName();
Integer version = pd.getVersion();
```

#### ProcessInstance (`com.openkm.okmflow.bean.ProcessInstance`)

Running instance of the process. Available in the context as `processInstance`.

| Property | Type   | Description |
|----------|--------|-------------|
| `id`     | `Long` | Unique identifier of the instance. |

```groovy
def procIns = context.get("processInstance");
long piId = procIns.getId();
```

#### TaskInstance (`com.openkm.okmflow.bean.TaskInstance`)

Instance of a workflow task.

| Property | Type   | Description |
|----------|--------|-------------|
| `id`     | `Long` | Unique identifier of the task. |

---

### DTOs returned by WorkflowUtils

These classes are returned by `WorkflowUtils` methods (not directly in the context).

#### ProcessDefinitionDTO (`com.openkm.okmflow.rest.dto.ProcessDefinitionDTO`)

Returned by `WorkflowUtils.getProcessDefinitionByName()`.

| Property | Type            | Description |
|----------|-----------------|-------------|
| `id`     | `Long`          | Unique identifier. |
| `name`   | `String`        | Process name. |
| `version`| `Integer`       | Version. |
| `locked` | `Boolean`       | Whether the definition is locked for editing. |
| `nodes`  | `List<NodeDTO>` | List of process nodes. |

```groovy
ProcessDefinitionDTO pd = WorkflowUtils.getProcessDefinitionByName("Manager-voting");
long pdId = pd.getId();
```

#### ProcessInstanceDTO (`com.openkm.okmflow.rest.dto.ProcessInstanceDTO`)

Returned by `WorkflowUtils.getProcessInstance()` and `getProcessInstancesByProcessName()`.

| Property       | Type                              | Description |
|----------------|-----------------------------------|-------------|
| `id`           | `Long`                            | Unique identifier. |
| `procDefName`  | `String`                          | Process name. |
| `status`       | `String`                          | Current status of the instance. |
| `lastActivity` | `LocalDateTime`                   | Timestamp of the last activity. |
| `currentNode`  | `NodeDTO`                         | Node where the instance is currently waiting. |
| `formVariables`| `Map<String, FormElementComplex>` | Form variables in the context. |
| `miscVariables`| `Map<String, Object>`             | Miscellaneous variables in the context. |

```groovy
ProcessInstanceDTO pi = WorkflowUtils.getProcessInstance(piId);
String status = pi.getStatus();
String currentNodeName = pi.getCurrentNode().getName();
```

#### TaskInstanceDTO (`com.openkm.okmflow.rest.dto.TaskInstanceDTO`)

Returned by `WorkflowUtils.getCurrentTaskInstance()` and `getTaskInstance()`.

| Property      | Type            | Description |
|---------------|-----------------|-------------|
| `id`          | `Long`          | Unique identifier. |
| `name`        | `String`        | Task name. |
| `status`      | `String`        | Current status (`open`, `assigned`, etc.). |
| `actor`       | `String`        | Assigned user. |
| `pooledActors`| `Set<String>`   | Pool users who can claim the task. |
| `start`       | `LocalDateTime` | Task start timestamp. |
| `end`         | `LocalDateTime` | Task completion timestamp. |
| `form`        | `String`        | XML definition of the associated form. |

```groovy
TaskInstanceDTO task = WorkflowUtils.getCurrentTaskInstance(processInstanceId);
if (task != null && "Waiting Task".equals(task.getName())) {
    WorkflowUtils.setTaskInstanceValues(processInstanceId, task.getName(), null, props);
}
```

---

## Database — Schema

The OKMFlow engine uses a relational database to store all workflow-related information. The schema is organised into two sections:

- **Definition tables:** store the structure and configuration of processes, including nodes, transitions, and their properties.
- **Execution tables:** store the runtime state of process instances, task assignments, variables, and execution history.

> **Important:** never manually modify execution tables while workflows are running. Changes to `PI_CURRENT_NODE_ID` or `TI_STATUS` can cause execution failures and data corruption. Always use the engine API for any runtime modifications.

### Definition tables

Store the structure and configuration of processes. `WF_PROCESS_DEFINITION` is the root; nodes use JPA JOINED inheritance strategy, meaning each specialised node type has its own table joined to the base via FK.

| Table                  | Description |
|------------------------|-------------|
| `WF_PROCESS_DEFINITION`| Process definitions: name, version, active status. |
| `WF_NODE`              | Base table for all nodes (position, name, description). Each node type has its own table that JOINs this one via JOINED inheritance. |
| `WF_NODE_START`        | Start nodes (inherits from `WF_NODE`). |
| `WF_NODE_END`          | End nodes (inherits from `WF_NODE`). |
| `WF_NODE_ACTION`       | Action nodes with script (inherits from `WF_NODE`). |
| `WF_NODE_DECISION`     | Decision nodes with conditional logic (inherits from `WF_NODE`). |
| `WF_NODE_MAIL`         | Mail nodes with email configuration (inherits from `WF_NODE`). |
| `WF_NODE_TASK`         | Task nodes with assignment and notifications (inherits from `WF_NODE`). |
| `WF_TRANSITION`        | Transitions connecting nodes, defining workflow execution paths. |

**Key relationships:**
- `WF_PROCESS_DEFINITION` → `WF_NODE`: 1-N. Each process contains multiple nodes that define the workflow structure.
- `WF_NODE` → specialised tables: JOINED inheritance via FK. Each type (Start, End, Action, Decision, Mail, Task) extends `WF_NODE`.
- `WF_NODE` → `WF_TRANSITION` (source): 1-N. A node may have multiple outgoing transitions.
- `WF_NODE` → `WF_TRANSITION` (target): 1-N. A node may have multiple incoming transitions.

**Useful queries:**

```sql
-- Active definitions
SELECT PD_ID, PD_NAME, PD_VERSION, PD_ACTIVE FROM WF_PROCESS_DEFINITION WHERE PD_ACTIVE = 'T' ORDER BY PD_NAME, PD_VERSION;

-- Nodes of a process
SELECT n.NOD_ID, n.NOD_NAME, n.NOD_POS_X, n.NOD_POS_Y FROM WF_NODE n
INNER JOIN WF_PROCESS_DEFINITION p ON n.NOD_PROCESS_DEF_ID = p.PD_ID
WHERE p.PD_NAME = 'document_approval' ORDER BY n.NOD_NAME;

-- Task nodes with notification enabled
SELECT n.NOD_ID, n.NOD_NAME, t.NTSK_NOTIF_ASSIGN, t.NTSK_NOTIF_SUBJECT
FROM WF_NODE n INNER JOIN WF_NODE_TASK t ON n.NOD_ID = t.NOD_ID
WHERE t.NTSK_NOTIF_ASSIGN = 'T';

-- Transitions between nodes
SELECT t.ID, t.TRA_NAME, t.TRA_TYPE, ns.NOD_NAME AS SOURCE_NODE, nt.NOD_NAME AS TARGET_NODE
FROM WF_TRANSITION t
INNER JOIN WF_NODE ns ON t.TRA_SOURCE_NODE_ID = ns.NOD_ID
INNER JOIN WF_NODE nt ON t.TRA_TARGET_NODE_ID = nt.NOD_ID;

-- Count nodes by type in a process
SELECT COUNT(*) AS START_NODES FROM WF_NODE n INNER JOIN WF_NODE_START s ON n.NOD_ID = s.NOD_ID WHERE n.NOD_PROCESS_DEF_ID = 1;
SELECT COUNT(*) AS TASK_NODES FROM WF_NODE n INNER JOIN WF_NODE_TASK t ON n.NOD_ID = t.NOD_ID WHERE n.NOD_PROCESS_DEF_ID = 1;
SELECT COUNT(*) AS DECISION_NODES FROM WF_NODE n INNER JOIN WF_NODE_DECISION d ON n.NOD_ID = d.NOD_ID WHERE n.NOD_PROCESS_DEF_ID = 1;
```

### Execution tables

Store the runtime state of process instances. These tables capture process instances, task assignments, variables, and execution history, forming a complete audit trail.

| Table                     | Description |
|---------------------------|-------------|
| `WF_PROCESS_INSTANCE`     | Process instances: current status, progress. |
| `WF_TASK_INSTANCE`        | Tasks assigned during execution. Includes the collection table `WF_TASK_INS_POOLED_ACTORS` to store potential assignees when the task is in a pool (several users can claim it, but only one ends up assigned). |
| `WF_VARIABLE`             | Process variables (persisted context). |
| `WF_PROCESS_INSTANCE_LOG` | Execution history: nodes visited, events, audit trail. |

**Key relationships:**
- `WF_PROCESS_INSTANCE` → `WF_PROCESS_DEFINITION`: N-1. Each instance is created from a specific version of a process definition.
- `WF_PROCESS_INSTANCE` → `WF_NODE`: N-1. Node where execution is currently positioned.
- `WF_PROCESS_INSTANCE` → `WF_TASK_INSTANCE`: 1-N. An instance generates multiple tasks throughout its execution.
- `WF_PROCESS_INSTANCE` → `WF_VARIABLE`: 1-N. Each instance maintains its own set of variables.
- `WF_PROCESS_INSTANCE` → `WF_PROCESS_INSTANCE_LOG`: 1-N. Execution generates multiple log entries.
- `WF_TASK_INSTANCE` → `WF_NODE_TASK`: N-1.

**Useful queries:**

```sql
-- Running instances
SELECT pi.PI_ID, pd.PD_NAME, pi.PI_STATUS, pi.PI_INITIATOR, pi.PI_START
FROM WF_PROCESS_INSTANCE pi
INNER JOIN WF_PROCESS_DEFINITION pd ON pi.PI_PROCESS_DEF_ID = pd.PD_ID
WHERE pi.PI_STATUS = 'running' ORDER BY pi.PI_START DESC;

-- Tasks assigned to a user
SELECT ti.TI_ID, ti.TI_STATUS, ti.TI_DUE_DATE, pi.PI_ID AS PROCESS_ID, pd.PD_NAME AS PROCESS_NAME
FROM WF_TASK_INSTANCE ti
INNER JOIN WF_PROCESS_INSTANCE pi ON ti.TI_PROCESS_INS_ID = pi.PI_ID
INNER JOIN WF_PROCESS_DEFINITION pd ON pi.PI_PROCESS_DEF_ID = pd.PD_ID
WHERE ti.TI_ACTOR = 'jsmith' AND ti.TI_STATUS IN ('open', 'assigned') ORDER BY ti.TI_DUE_DATE;

-- Variables of an instance
SELECT VAR_NAME, VAR_CLASS_NAME, VAR_VALUE FROM WF_VARIABLE
WHERE VAR_PROCESS_INSTANCE_ID = 123 ORDER BY VAR_NAME;

-- Execution history
SELECT PIL_DATE, PIL_NODE_TYPE, PIL_NODE_NAME, PIL_STATUS, PIL_DATA
FROM WF_PROCESS_INSTANCE_LOG WHERE PIL_PROC_INS_ID = 123 ORDER BY PIL_DATE ASC;

-- Overdue tasks
SELECT ti.TI_ID, ti.TI_ACTOR, ti.TI_DUE_DATE, pd.PD_NAME AS PROCESS_NAME, tn.NOD_NAME AS TASK_NAME
FROM WF_TASK_INSTANCE ti
INNER JOIN WF_PROCESS_INSTANCE pi ON ti.TI_PROCESS_INS_ID = pi.PI_ID
INNER JOIN WF_PROCESS_DEFINITION pd ON pi.PI_PROCESS_DEF_ID = pd.PD_ID
INNER JOIN WF_NODE_TASK tn ON ti.TI_TASK_NODE_ID = tn.NOD_ID
WHERE ti.TI_STATUS IN ('open', 'assigned') AND ti.TI_DUE_DATE < CURRENT_TIMESTAMP ORDER BY ti.TI_DUE_DATE;

-- Pool tasks available to a user
SELECT DISTINCT ti.TI_ID, ti.TI_STATUS, ti.TI_DUE_DATE, pd.PD_NAME AS PROCESS_NAME, tn.NOD_NAME AS TASK_NAME
FROM WF_TASK_INSTANCE ti
INNER JOIN WF_TASK_INS_POOLED_ACTORS pa ON ti.TI_ID = pa.TIPA_TASK_INS_ID
INNER JOIN WF_PROCESS_INSTANCE pi ON ti.TI_PROCESS_INS_ID = pi.PI_ID
INNER JOIN WF_PROCESS_DEFINITION pd ON pi.PI_PROCESS_DEF_ID = pd.PD_ID
INNER JOIN WF_NODE_TASK tn ON ti.TI_TASK_NODE_ID = tn.NOD_ID
WHERE pa.TIPA_ACTOR = 'jsmith' AND ti.TI_STATUS = 'open' ORDER BY ti.TI_DUE_DATE;
```

---

## API — Utility classes

Classes available in workflow Groovy scripts:

| Class             | Package                        | Description |
|-------------------|--------------------------------|-------------|
| `FileLogger`      | `com.openkm.util`              | Log generation (info, warn, error). |
| `FileUtils`       | `com.openkm.util`              | Utilities for file names and extensions. |
| `PathUtils`       | `com.openkm.util`              | Utilities for OpenKM paths. |
| `StackTraceUtils` | `com.openkm.okmflow.util`      | Converts exceptions to text. |
| `WorkflowUtils`   | `com.openkm.okmflow.util`      | Utilities for workflow operations. |

---

## FileLogger

`import com.openkm.util.FileLogger;`

Writes messages to a log file identified by `baseName`. Supports `printf`-style formatting with optional parameters.

| Method | Return | Description |
|--------|--------|-------------|
| `info(String baseName, String message, Object... params)` | `void` | Writes an informational message. |
| `warn(String baseName, String message, Object... params)` | `void` | Writes a warning message. |
| `error(String baseName, String message, Object... params)` | `void` | Writes an error message. |

```groovy
import com.openkm.util.FileLogger;

FileLogger.info("my-workflow", "Starting process for uuid: %s", uuid);
FileLogger.warn("my-workflow", "Variable not found: %s", "invoiceNumber");
FileLogger.error("my-workflow", "Error processing: %s %s", "detail1", "detail2");
```

---

## FileUtils

`import com.openkm.util.FileUtils;`

Utilities for working with file names.

| Method | Return | Description |
|--------|--------|-------------|
| `getFileName(String file)` | `String` | Returns the file name without extension. |
| `getFileExtension(String file)` | `String` | Returns the file extension. |

```groovy
import com.openkm.util.FileUtils;
import com.openkm.util.PathUtils;

// node.getPath() returns "/okm:root/invoices/invoice-001.pdf"
String docName  = PathUtils.getName(node.getPath());       // "invoice-001.pdf"
String baseName = FileUtils.getFileName(docName);          // "invoice-001"
String extension = FileUtils.getFileExtension(docName);    // "pdf"
```

---

## PathUtils

`import com.openkm.util.PathUtils;`

Utilities for working with OpenKM node paths.

| Method | Return | Description |
|--------|--------|-------------|
| `getParent(String path)` | `String` | Returns the path of the parent node. |
| `getName(String path)` | `String` | Returns the node name (last segment of the path). |

```groovy
import com.openkm.util.PathUtils;

PathUtils.getParent("/okm:root/invoices/invoice-001.pdf"); // "/okm:root/invoices"
PathUtils.getName("/okm:root/invoices/invoice-001.pdf");   // "invoice-001.pdf"
```

---

## StackTraceUtils

`import com.openkm.okmflow.util.*;`

Utility to convert an exception to readable text, especially useful combined with `FileLogger.error`.

| Method | Return | Description |
|--------|--------|-------------|
| `toString(Throwable t)` | `String` | Returns the exception stack trace as text. |

```groovy
import com.openkm.okmflow.util.*;
import com.openkm.util.FileLogger;

try {
    // script logic
} catch (Exception e) {
    FileLogger.error("my-workflow", "Unexpected error: " + StackTraceUtils.toString(e));
}
```

---

## WorkflowUtils

`import com.openkm.okmflow.util.WorkflowUtils;`

Core utilities for operating with processes, instances, tasks, and form elements from Groovy scripts.

> **Important — only use documented methods.** Do not assume that a method exists because it seems logical or follows a naming pattern. If a required operation is not covered by the methods listed below, ask the user to confirm the exact method signature before writing code that uses it.

### Forms and context

| Method | Return | Description |
|--------|--------|-------------|
| `getFormElementValue(Map<String,Object> context, String name)` | `String` | Gets the value of a form element from the context. |
| `getFormElementValueMap(Map<String,Object> context, List<String> names)` | `Map<String,String>` | Gets multiple form values from the context. |
| `formElementToPropertyMap(Map<String,Object> context, String prefix, List<String> names)` | `Map<String,String>` | Converts form elements to a property map with a prefix. |
| `convertToListFromSelectValue(String value)` | `List<String>` | Converts a `;`-separated `String` to `List<String>`. |
| `convertToSelectValue(List<String> values)` | `String` | Converts a `List<String>` to a `;`-separated `String`. |

```groovy
// Get a single value from context
String value = WorkflowUtils.getFormElementValue(context, "invoiceNumber");

// Get multiple values at once
List<String> names = ["invoiceNumber", "amount", "department"];
Map<String,String> values = WorkflowUtils.getFormElementValueMap(context, names);

// Convert to a property map for setProperties()
Map<String,String> props = WorkflowUtils.formElementToPropertyMap(context, "okp:invoice", ["number", "total"]);
// Result: {"okp:invoice.number": "...", "okp:invoice.total": "..."}

// Select <-> List conversion
List<String> list = WorkflowUtils.convertToListFromSelectValue("user1;user2;user3");
String selectVal = WorkflowUtils.convertToSelectValue(["user1", "user2", "user3"]);
```

### Process instance variables

| Method | Return | Description |
|--------|--------|-------------|
| `addProcessInstanceVariable(Long piId, String key, Object value)` | `void` | Adds or updates a variable in another process instance's context. |
| `removeProcessInstanceVariable(Long piId, String key)` | `void` | Removes a variable from a process instance's context. |
| `getProcessInstanceVariable(Long piId, String key)` | `Object` | Gets the value of a variable from a process instance. |

```groovy
WorkflowUtils.addProcessInstanceVariable(parentPiId, "status_manager1", "ENDED");
WorkflowUtils.removeProcessInstanceVariable(piId, "tempValue");
Object status = WorkflowUtils.getProcessInstanceVariable(piId, "status");
```

### Process instances and definitions

| Method | Return | Description |
|--------|--------|-------------|
| `getProcessInstance(Long piId)` | `ProcessInstanceDTO` | Gets a process instance by ID. |
| `getProcessInstancesByProcessName(String procDefName)` | `List<ProcessInstanceDTO>` | Gets all active instances of a process by name. |
| `getProcessDefinitionByName(String procDefName)` | `ProcessDefinitionDTO` | Gets the process definition by name. |
| `runProcessDefinition(Long pdId, Map<String,Object> data)` | `CompletableFuture<Long>` | Launches a new process instance asynchronously. Returns the instance ID. |
| `endProcessInstance(Long piId)` | `CompletableFuture<Void>` | Ends a process instance asynchronously. |
| `setProcessInstanceNode(Map<String, Object> context, String uuid)` | `void` | Changes the OpenKM node associated with a process instance. Reads the node from OpenKM using `uuid` and updates `uuid`, `node` and `nodeName` directly in the given `context` map (in-place). Does not take the process instance ID as a parameter. |

```groovy
// Get the definition and start a new child process
def pd = WorkflowUtils.getProcessDefinitionByName("Manager-voting");
def props = [uuid: uuid, initiator: initiatorId, actor: manager];
Long childPiId = WorkflowUtils.runProcessDefinition(pd.getId(), props).get();

// End a process
WorkflowUtils.endProcessInstance(childPiId).get();

// Attach the workflow process instance to a specific repository node
WorkflowUtils.setProcessInstanceNode(context, uuid);
```

### Tasks

| Method | Return | Description |
|--------|--------|-------------|
| `getTaskInstance(Long piId, String taskName)` | `TaskInstanceDTO` | Gets a task by name within a process instance. |
| `getTaskInstances(Long piId, String taskName)` | `List<TaskInstanceDTO>` | Returns all historical instances of a task by name within a process instance, ordered by id ASC. Useful for recovering previous actors or checking how many times a task has been executed. |
| `getCurrentTaskInstance(Long piId)` | `TaskInstanceDTO` | Gets the currently waiting task of a process instance. |
| `setTaskInstanceValues(Long piId, String taskName, String transName, Map<String,Object> data)` | `CompletableFuture<Void>` | Programmatically completes a task, optionally following a transition with data. |

> `TaskInstanceDTO` belongs to `com.openkm.okmflow.rest.dto`. It is **not** included in `com.openkm.okmflow.util.*` or `com.openkm.okmflow.bean.*`. Always import `com.openkm.okmflow.rest.dto.*` alongside `WorkflowUtils` whenever a script uses `TaskInstanceDTO` (e.g. via `getTaskInstance`, `getTaskInstances`, `getCurrentTaskInstance`) — omitting it causes `unable to resolve class TaskInstanceDTO` at runtime.

> **`taskName` is not limited to the calling task itself.** `getTaskInstances(piId, taskName)` accepts **any** task node's `name` in the process definition — not necessarily the name of the task whose `assignExpr` is calling it, and not necessarily an adjacent node. This makes it possible to route a task to the actor of any earlier task in the graph, which is useful when a later step conceptually "belongs" to whoever handled an earlier, unrelated step (e.g. escalating a downstream rejection back to an upstream approver several nodes earlier), not just for re-assigning a task to whoever last handled that same task on a previous pass.

```groovy
import com.openkm.okmflow.util.*;
import com.openkm.okmflow.rest.dto.*;   // required for TaskInstanceDTO
import java.util.*;

// Check the current task and complete it if a condition is met
TaskInstanceDTO task = WorkflowUtils.getCurrentTaskInstance(processInstanceId);
if ("Waiting Task".equals(task.getName())) {
    def props = [uuid: uuid];
    WorkflowUtils.setTaskInstanceValues(processInstanceId, "Waiting Task", null, props).get();
}

// Re-assign a task to the same actor who handled it previously (must return String, not List)
def procIns = context.get("processInstance");
long piId = procIns.getId();
List<TaskInstanceDTO> prevTasks = WorkflowUtils.getTaskInstances(piId, "Manager approval");
if (prevTasks != null && !prevTasks.isEmpty()) {
    for (TaskInstanceDTO t : prevTasks) {
        if (t.getActor() != null && !t.getActor().isEmpty()) {
            return t.getActor(); // String → direct assignment, not pool
        }
    }
}
```

