DxpLife The practical Liferay DXP knowledge hub
Talk to us

Home/Knowledge Hub/Workflow & process

Workflow & process

Multi-Level Approval Workflow in Liferay DXP

Five to seven approval levels with scripted round-robin assignment in Kaleo

In-depth guideAdvanced39 sections~15 min read
In this resource. Design a seven-level approval workflow in Kaleo with scripted round-robin assignment, including state handling across nodes, concurrency, rejection paths, escalation, auditing and production hardening.

Introduction to Liferay Workflow

Liferay DXP provides a workflow framework commonly known as Kaleo Workflow. Workflow allows organizations to define controlled business processes around assets and records. Typical use cases include:

  • Content publication approval
  • Expense approval
  • Purchase requests
  • Employee onboarding
  • Legal document review
  • Contract approval
  • Product approval
  • Compliance review
  • Customer onboarding
  • Custom Liferay Object approval

A workflow describes:

Who performs what action, in what order, under what conditions

Example Business Requirement

Assume an enterprise application requires a seven-level approval process. The workflow is:

Submitter → Level 1 → Level 2 → Level 3 → Level 4 → Level 5 → Level 6 → Level 7 → Approved

Each level contains multiple eligible approvers. Instead of assigning every request to the same person, requests should be distributed among eligible approvers using a round-robin strategy. For example, if Level 1 contains:

U1 , U2 , U3

then successive assignments should ideally be:

U1 → U2 → U3 → U1 → U2 → · · ·

Why Use Round-Robin Assignment?

Without an assignment strategy, one approver may receive disproportionately more work than others. Round-robin assignment attempts to distribute tasks sequentially. Suppose:

A = {A1 , A2 , A3 , A4 }

is the set of available approvers. For workflow request number n, a basic assignment rule is:

i = nmod |A|

and:

Assignee(n) = Ai

For four approvers:

Request IndexAssigned User
1 0User A
2 1User B
3 2User C
4 3User D
5 0User A
6 1User B

Workflow Building Blocks

A Liferay workflow commonly uses the following elements.

State

A state represents a stage or status in the workflow. Examples:

  • Created
  • Submitted
  • Approved
  • Rejected

Task

A task represents work that must be performed by a user. Examples:

  • Manager Approval
  • Finance Approval
  • Legal Review

Transition

A transition moves the workflow from one node to another. Typical transitions include:

  • Approve
  • Reject
  • Resubmit
  • Escalate

Assignment

Assignment determines who receives responsibility for a task. An assignment can conceptually target:

  • User
  • Role
  • Organization Role
  • Asset Creator
  • Script-generated assignee

Condition

A condition determines the next workflow path using business logic. For example:

amount > 1, 000, 000

could send the request to an additional executive approval level.

Seven-Level Workflow Architecture

Consider the following approval hierarchy.

LevelTaskExample Responsibility
1Initial ReviewOperational Reviewer
2Supervisor ApprovalTeam Lead / Supervisor
3Manager ApprovalDepartment Manager
4Finance ReviewFinance Team
LevelTaskExample Responsibility
5Compliance ReviewCompliance Team
6Director ApprovalBusiness Director
7Final ApprovalExecutive / Final Authority

The basic process is:

Diagram
START
  |
  v
Level 1 Review
  |
  v
Level 2 Approval
  |
  v
Level 3 Approval
  |
  v
Level 4 Finance
  |
  v
Level 5 Compliance
  |
  v
Level 6 Director
  |
  v
Level 7 Final Approval
  |
  v
APPROVED

Workflow with Rejection Paths

A production workflow also needs rejection handling. For example:

Diagram
+----------------+
|    Submitted   |
+-------+--------+
       |
       v
    Level 1
       |
   Approve
       v
    Level 2
       |
   Approve
       v
    Level 3
Diagram
                       |
                      ...
                       |
                       v
                    Level 7
                       |
                   Approve
                       v
                   APPROVED

Any Level
    |
    +------ Reject ------> REJECTED

Alternatively, rejection can return the item to the submitter:

Diagram
Level N
   |
   | Reject
   v
Needs Revision
   |
   | Resubmit
   v
Level 1

Round-Robin Assignment Concept

Suppose Level 1 contains three users:

Code
reviewer1
reviewer2
reviewer3

The assignment sequence should be:

Diagram
Request 001 -> reviewer1
Request 002 -> reviewer2
Request 003 -> reviewer3
Request 004 -> reviewer1
Request 005 -> reviewer2
Request 006 -> reviewer3

Mathematically:

nextIndex = (currentIndex + 1) mod N

where:

N = number of eligible approvers

This ensures that after reaching the final user, assignment starts again from the first user.

Why Scripted Assignment?

Static assignment works well when every workflow task belongs to a fixed role. For example:

Code
Finance Approval -> Finance Manager Role

However, scripted assignment becomes useful when the assignee depends on runtime logic. Examples include:

  • Round-robin assignment
  • Region-based assignment
  • Department-based routing
  • Amount-based assignment
  • Customer category
  • Product category
  • Current workload
  • Availability
  • Shift
  • Organization hierarchy

Conceptual Groovy Round-Robin Script

The following example illustrates the core algorithm.

Script
def approverUserIds = [
    20101L,
    20102L,
    20103L
]

def currentIndex = getCurrentRoundRobinIndex()

def selectedIndex =
    currentIndex % approverUserIds.size()

def selectedUserId =
    approverUserIds[selectedIndex]

setNextRoundRobinIndex(
    (selectedIndex + 1) % approverUserIds.size()
)
Code
// Return or expose selectedUserId
// according to the assignment contract
// expected by the Liferay workflow definition.

The central algorithm is:

iselected = icurrent mod N

followed by:

inext = (iselected + 1) mod N

Avoid Hard-Coded Users

Hard-coding user IDs is usually unsuitable for production. Instead of:

Script
def users = [20101L, 20102L, 20103L]

eligible users should preferably be derived from a stable business concept such as a Liferay role. For example:

Script
def role = RoleLocalServiceUtil.getRole(
    companyId,
    "Level 1 Approver"
)

def users =
    UserLocalServiceUtil.getRoleUsers(
       role.getRoleId()
    )

Conceptually:

Role → Eligible Users → Round Robin → Selected User

This makes administration significantly easier.

Filtering Eligible Users

A robust implementation may need to filter users. For example:

Script
def eligibleUsers = users.findAll { user ->

    user.isActive()

}

Additional enterprise filters could consider:

  • Active/inactive status
  • Organization membership
  • Business unit
  • Geographic region
  • Leave status
  • Shift
  • Required permissions
  • Approval limits

Thus the algorithm becomes:

Role Members → Eligibility Filter → Round Robin → Assignment

Where Should the Counter Be Stored?

This is one of the most important architectural questions. Suppose the script stores:

Properties
currentIndex = 2

only in memory. After a server restart, that value may disappear. In a cluster:

Code
Node A -> counter = 3
Node B -> counter = 1

The nodes could then produce inconsistent assignments. Therefore, persistent state should be considered. Possible approaches include:

  • Custom database table
  • Liferay Object
  • Dedicated OSGi service
  • Counter/persistence service
  • External coordination service

Concurrency Problem

Consider two workflow instances arriving simultaneously. Both read:

currentIndex = 2

Both calculate:

2 mod 3 = 2

Therefore, both assign the same user. This is a race condition. The desired operation should conceptually be atomic:

Read → Select → Increment → Persist

as one controlled operation. Possible solutions include:

  • Database locking
  • Atomic database update
  • Transactional service
  • Distributed locking
  • Optimistic locking with retry

For complex applications, use:

Diagram
Kaleo Workflow
      |
      v
Groovy Assignment Script
      |
      v
RoundRobinAssignmentService
      |
      +----> Find Eligible Users
      |
      +----> Read Assignment State
      |
      +----> Select Next User
      |
      +----> Update State
      |
      +----> Return Assignee

The script remains small:

Script
def selectedUserId =
    roundRobinAssignmentService
       .getNextApprover(
          companyId,
          "LEVEL_1_APPROVER"
       )

The business logic lives in a testable Java/OSGi service.

Separate Round Robin Per Level

Each approval level should normally maintain its own assignment pool. For example:

LevelRole Counter
1LEVEL_1_APPROVER RR_L1
2LEVEL_2_APPROVER RR_L2
3LEVEL_3_APPROVER RR_L3
4FINANCE_APPROVER RR_L4
5COMPLIANCE_APPROVER RR_L5
6DIRECTOR_APPROVER RR_L6
7FINAL_APPROVER RR_L7

This prevents the activity at one approval level from affecting another level’s rotation.

Generic Assignment Function

Instead of writing seven different algorithms, define one generic operation:

f(companyId, roleName, queueKey) → UserId

For example:

Code
long getNextApprover(
    long companyId,
    String roleName,
    String queueKey
)

Usage:

Code
getNextApprover(
    companyId,
    "Level 1 Approver",
    "RR_LEVEL_1"
)

getNextApprover(
    companyId,
    "Finance Approver",
    "RR_LEVEL_4"
)

getNextApprover(
    companyId,
    "Final Approver",
    "RR_LEVEL_7"
)

This promotes reuse and maintainability.

Conceptual Kaleo Workflow Definition

The following is intentionally simplified to illustrate the structure rather than serve as a drop-in definition for every Liferay release.

Diagram
<workflow-definition>

    <name>seven-level-round-robin</name>

    <state>
       <name>created</name>

       <initial>true</initial>

       <transitions>
          <transition>
             <name>submit</name>
             <target>level-1-review</target>
          </transition>
       </transitions>
    </state>

    <task>
       <name>level-1-review</name>

       <assignments>
          <!-- Scripted assignment logic -->
       </assignments>

       <transitions>

          <transition>
             <name>approve</name>
             <target>level-2-review</target>
          </transition>

          <transition>
             <name>reject</name>
             <target>rejected</target>
          </transition>

       </transitions>

    </task>

    <!-- Levels 2 through 7 -->

    <state>
       <name>approved</name>
    </state>

    <state>
       <name>rejected</name>
    </state>

</workflow-definition>

Workflow Context

Workflow scripts can work with contextual information associated with the running workflow instance. Useful values can include concepts such as:

  • Company ID
  • Group/Site ID
  • User ID
  • Entry class name
  • Entry class primary key
  • Workflow instance information
  • Asset-related information

Conceptually:

Script
def companyId = workflowContext.get("companyId")
def groupId = workflowContext.get("groupId")
def userId = workflowContext.get("userId")

These values can drive dynamic assignment.

Context-Aware Round Robin

A more advanced algorithm can maintain separate queues. For example:

Q = (Company, Site, Department, Level)

Therefore:

Diagram
Delhi + Finance + Level 4
    -> Finance Queue A

Mumbai + Finance + Level 4
    -> Finance Queue B

Delhi + Compliance + Level 5
    -> Compliance Queue C

The round-robin key might therefore be:

Code
companyId + ":" +
groupId + ":" +
department + ":" +
approvalLevel

This allows the assignment engine to scale across multiple organizational units.

Example Seven-Level Design

A possible workflow is:

Diagram
SUBMIT
   |
   v
+-----------------------+
| Level 1               |
| Initial Review        |
| Round Robin           |
+-----------+-----------+
           |
           v
+-----------------------+
| Level 2               |
| Supervisor            |
| Round Robin           |
+-----------+-----------+
           |
           v
+-----------------------+
| Level 3               |
| Manager               |
| Round Robin           |
+-----------+-----------+
           |
           v
+-----------------------+
| Level 4               |
| Finance               |
| Round Robin           |
+-----------+-----------+
           |
           v
+-----------------------+
| Level 5               |
| Compliance            |
| Round Robin           |
+-----------+-----------+
           |
           v
+-----------------------+
| Level 6               |
| Director              |
| Round Robin           |
+-----------+-----------+
           |
           v
+-----------------------+
| Level 7               |
| Final Authority       |
| Round Robin           |
Diagram
+-----------+-----------+
           |
           v
       APPROVED

Conditional Approval Levels

Not every request necessarily needs all seven approvals. For example:

Amount < 100, 000

may require only three levels. While:

100, 000 ≤ Amount < 1, 000, 000

may require five levels. And:

Amount ≥ 1, 000, 000

may require all seven levels. Conceptually:

Diagram
       +------------------+
       | Check Amount     |
       +--------+---------+
               |
  +-------------+-------------+
  |            |           |
  v            v           v
3 Levels      5 Levels      7 Levels

This can be modeled with workflow conditions.

Fallback Strategy

What happens if a role contains no eligible users? A robust workflow should define a fallback. For example:

Eligible Users = ∅

Then:

Assignment → Fallback Role

Possible fallback strategies include:

  • Assign to Workflow Administrator
  • Assign to Department Head
  • Assign to a fallback role
  • Pause workflow and notify administrators
  • Escalate to the next authority

Never silently leave the task unassigned.

Escalation

Suppose an approver does not respond within a specified SLA. Example:

Twaiting > 48 hours

Then:

Task → Escalation

Possible escalation actions include:

  • Notify the current approver
  • Notify the manager
  • Reassign the task
  • Move to an escalation role
  • Increase task priority

Round robin can therefore be combined with SLA management.

Notifications

Notifications can be associated with workflow events. For example:

Diagram
Task Assigned
     |
     +--> Email Approver
     |
     +--> Portal Notification
     |
     +--> Optional external integration

Useful notification events include:

  • Task assignment
  • Approval
  • Rejection
  • Escalation
  • Final approval
  • Workflow completion

Audit Trail

A multi-level workflow should preserve an auditable history. For each decision, useful information includes:

  • Workflow instance
  • Task
  • Assignee
  • Actual reviewer
  • Assignment time
  • Completion time
  • Transition selected
  • Comments
  • Previous state
  • Next state

Conceptually:

LevelApproverAction Time
1User AApproved 10:15
2User DApproved 11:32
3User FApproved 13:05
4User HApproved 14:45
5User JApproved 15:30

Round Robin vs Load Balancing

Round robin does not necessarily mean equal workload. Suppose:

U1 = 15 active tasks

and:

U2 = 2 active tasks

A simple round-robin algorithm may still select U1. A workload-aware strategy could instead choose:

∗
U = arg min ActiveTasks(u)
u∈U

This selects the eligible user with the lowest current workload. Therefore:

StrategyBasisBenefit
Round RobinSequenceSimple/fair rotation
Least LoadedActive tasksWorkload balancing
Weighted Round RobinUser weightCapacity awareness
Skill BasedUser capabilityExpertise matching
Region BasedGeographyLocal ownership

Weighted Round Robin

Suppose senior reviewers should receive twice as many requests as junior reviewers. Weights could be:

wA = 2, wB = 1, wC = 1

A conceptual sequence becomes:

A, A, B, C, A, A, B, C, . . .

Weighted round robin can be useful when approvers have different capacities.

Transaction and Cluster Considerations

For a clustered Liferay environment:

Diagram
        Load Balancer
            |
     +-------+-------+
     |             |
     v             v
Liferay Node 1   Liferay Node 2
     |             |
     +-------+-------+
            |
            v
      Shared Database

The assignment state must remain consistent across nodes. A purely local counter such as:

Code
static int counter = 0;

is unsuitable. Each JVM would maintain a different value. The preferred design is:

Shared Persistent State + Atomic Update

Security Considerations

Scripted workflow logic should be treated as privileged server-side logic. Important controls include:

  • Restrict who can modify workflow definitions
  • Avoid arbitrary user-controlled script execution
  • Validate IDs and contextual inputs
  • Use Liferay service APIs correctly
  • Respect permissions
  • Avoid exposing sensitive information in logs
  • Keep scripts small and maintainable

For a seven-level enterprise workflow, a clean architecture is:

Diagram
+-------------------------+
| Liferay Asset / Object  |
+------------+------------+
            |
            v
+-------------------------+
| Kaleo Workflow          |
+------------+------------+
            |
            v
+-------------------------+
| Scripted Assignment     |
| Adapter                 |
+------------+------------+
            |
            v
+-------------------------+
| RoundRobin Service      |
+------------+------------+
            |
      +------+------+
      |           |
      v           v
 Role Service    State Store
      |           |
      +------+------+
            |
            v
      Selected User
            |
            v
      Workflow Task

The responsibilities are separated as follows:

Kaleo Workflow Controls the business process.

Assignment Script Connects workflow execution to dynamic assignment logic.

Round-Robin Service Determines the next eligible approver.

Role Service Retrieves eligible users.

Persistent Store Maintains rotation state safely.

Pseudo Algorithm

A production-oriented algorithm can be expressed as:

Code
function getNextApprover(level):

    role = getRoleForLevel(level)

    users = getEligibleUsers(role)

    if users is empty:
       return fallbackApprover(level)

    sort users deterministically

    lock roundRobinState(level)

    currentIndex = getCurrentIndex(level)

    selectedIndex =
       currentIndex mod users.size()

    selectedUser =
       users[selectedIndex]

    nextIndex =
       (selectedIndex + 1) mod users.size()

    saveCurrentIndex(level, nextIndex)

    unlock roundRobinState(level)

    return selectedUser

The deterministic sort is important because a round-robin algorithm requires a stable ordering of users.

Example End-to-End Execution

Assume a request named:

Code
Purchase Request PR-10025

is submitted. The execution could be:

Code
PR-10025 submitted

Level 1
  Eligible: A, B, C
  Counter: 1
  Assigned: B
  Result: Approved

Level 2
  Eligible: D, E
  Counter: 0
YAML
  Assigned: D
  Result: Approved

Level 3
  Eligible: F, G, H
  Counter: 2
  Assigned: H
  Result: Approved

Level 4
  Eligible: Finance1, Finance2
  Assigned: Finance2
  Result: Approved

Level 5
  Eligible: Compliance1, Compliance2
  Assigned: Compliance1
  Result: Approved

Level 6
  Eligible: Director1, Director2
  Assigned: Director2
  Result: Approved

Level 7
  Eligible: Executive1, Executive2
  Assigned: Executive1
  Result: Approved

Final State: APPROVED

Failure Scenarios

A robust design should handle:

No Users in Role

Use a fallback or administrative queue.

Selected User Becomes Inactive

Re-evaluate eligibility before assignment.

Server Restart

Persistent counter/state prevents rotation loss.

Multiple Liferay Nodes

Use shared persistent state and concurrency controls.

Concurrent Requests

Use atomic state updates or locking.

Workflow Script Error

Log sufficient diagnostic information and ensure administrators can recover or reassign affected tasks.

Testing Strategy

Testing should include more than the normal approval path.

TestExpected Result
Normal 7-level approvalRequest reaches Approved state.
Reject at Level 3Configured rejection path executes.
Round-robin rotationUsers receive tasks in expected sequence.
Inactive userInactive user is skipped.
Empty roleFallback strategy executes.
Concurrent submissionsNo unintended duplicate counter selection.
Server restartRotation state remains consistent.
Cluster testAll nodes observe consistent state.
ResubmissionWorkflow returns through configured path.

Interview Questions and Answers

Multiple Choice Questions

Question 1

Which Liferay framework is commonly used for workflow?

A. Service Builder

B. Kaleo

C. Clay

D. Elasticsearch

Answer: B

Question 2

What is the primary purpose of round-robin assignment?

A. Encrypt workflow data

B. Sequentially distribute tasks among eligible users

C. Delete completed tasks

D. Create Liferay sites

Answer: B

Question 3

For N users, which expression can be used for cyclic selection?

A. i + N

B. i/N

C. i mod N

D. N − i2

Answer: C

Question 4

Why is an in-memory static counter problematic in a Liferay cluster?

A. It cannot store integers

B. Every node may have a different counter value

C. Groovy cannot read integers

D. Workflow tasks cannot use users

Answer: B

Question 5

Which approach is generally more suitable for complex production round-robin logic?

A. Large workflow scripts containing all business logic

B. Browser-side JavaScript

C. Dedicated service with persistent assignment state

D. JSP session variables

Answer: C

Question 6

What should happen if the eligible-user list is empty?

A. Divide by zero

B. Leave the task permanently lost

C. Execute a defined fallback strategy

D. Restart Liferay automatically

Answer: C

Question 7

What problem occurs if two requests read the same round-robin counter before either updates it?

A. Dead code

B. Race condition

C. Serialization error

D. Authentication failure

Answer: B

Question 8

Which strategy assigns work to the user with the fewest active tasks?

A. Pure round robin

B. Least-loaded assignment

C. Random assignment

D. Static role assignment

Answer: B

Question 9

Why should the eligible user list be deterministically ordered?

A. To reduce HTTP traffic

B. To provide a stable round-robin sequence

C. To encrypt user IDs

D. To create workflow states

Answer: B

Question 10

Which architecture is most appropriate for cluster-safe assignment?

A. JVM static variable

B. Browser localStorage

C. Shared persistent state with atomic updates

D. HTTP session only

Answer: C

Quick Revision Table

ConceptMeaning
KaleoLiferay workflow framework
TaskHuman or system work step
TransitionPath from one workflow node to another
AssignmentDetermines who owns a workflow task
Scripted AssignmentDynamic runtime assignment logic
Round RobinSequential cyclic distribution
Persistent CounterStores rotation position across executions
FallbackAlternative assignment when no normal assignee exists
EscalationAction when a task exceeds its SLA or requires intervention
Audit TrailHistorical record of workflow decisions

Key Takeaway

A scalable Liferay multi-level approval workflow can be represented as:

Submission → Workflow Level → Eligible Role → Round-Robin Engine → Specific Approver

For seven levels:

L1 → L2 → L3 → L4 → L5 → L6 → L7 → Approved

The round-robin operation at each level can be modeled as:

inext = (icurrent + 1) mod N

For production deployments, however, the key architectural concern is not merely the modulo calculation. The system must also handle persistent state, concurrency, clustering, inactive users, fallback assignment, auditing, rejection, resubmission, and administrative recovery. A strong design therefore keeps the Kaleo workflow focused on process orchestration while delegating complex assignment logic to a dedicated, testable service.

Official Reference

Liferay’s official introduction to Workflow is available at:

Code
https://learn.liferay.com/w/dxp/low-code/workflow/introduction-to-workflow

The exact workflow-definition schema, scripting availability, and supported workflow features should always be verified against the specific Liferay DXP version used by the target environment.

Prefer to read offline? Download the PDF edition (27 pages).