Sitemap

Push notification for approval workflow process tasks with Camunda

7 min readJun 9, 2020

--

Introduction

An approval workflow process (AWP) is a particular type of workflow that involves humans. Such workflows have multiple checks or steps throughout the process and from a variety of different stakeholders; can be users or groups of users. They are use to solve a variety of use cases where businesses must approve documents (e.g invoices) or requests (e,g time off, purchase orders…). A proper workflow engine such as Camunda can be used to build an automated approval system instead of relying on manual processes.

Press enter or click to view image in full size
A simple approval workflow process with three steps for payment requests

In Camunda, you can represent each step of a AWP by a user task.

A User Task is used to model work that needs to be done by a human actor. When the process execution arrives at such a User Task, a new task is created in the task list of the user(s) or group(s) assigned to that task.

After the initiation of a workflow, a task is assigned to one or several users or one or several groups of users. When the task is completed (approved or rejected), the workflow process instance move to the next step; a new task is created and assigned to one or several users and so on…

What we would like to have is a system that can push notifications to users when one or several tasks are assigned to them and be notified when an assigned task has been completed.

We can imagine to build a web UI that can display all the tasks per user, subscribe to notifications and get the tasks in real time via a WebSocket for instance.

Hopefully, Camunda is flexible enough to provide entry point to build and connect such system. Let’s see how it can be done in details by building a small prototype.

The full code source is available on github here along with unit tests and integration tests to let you play with it!

A simple use case

To fix ideas, let’s work with this simple AWP that represents an imaginary request.

Press enter or click to view image in full size

When a user initiates a new request, a workflow process instance kicks off. The first task is created and let’s say it has to be completed in this scenario by either user1, user2 or user3 or a user belonging to group1 or group2 (they are called respectively candidate users and candidates groups in Camunda). If approved, the second task is created and can only be completed by user4. If not, the process ends immediately (rejected branch). In our scenario, user1 belongs to group1, user2 to group1 and group2 and user3 and user4 does not belong to any group.

This BPMN model is defined in Java. Check out the code here.

Requirements

When the first task is created, we want to send a notification to user1, user2, user3 and all users from group1 and group2 if any of them have subscribed to the task notifications. When it is completed (approved or rejected), a new notification should be sent to the same list of users to indicate the new status and that the request does not need their approval anymore. In case of approved, a new notification should be sent to user4 to let him know a request is waiting for his approval corresponding to the second created task.

Here’s a couple of rules the implementation should respect:

  • When subscribing, a user should receive all notification tasks waiting for his approval. The associated notifications are considered as ‘active’. That means we have to store all notifications in a database.
  • After registration, a user should receive any new notification.
  • Notification can individually be marked as read by a user. Next time a user register to receive notifications, he won’t received the read ones.
  • Notifications can be marked as read in bulk by a user.
  • A notification should only be sent once. In our example, user1 belongs to group1 and user1 and group1 are candidates for the first task. We need to make sure that user1 receives only once the notification.
  • Notifications should be delivered in correct order i.e in the order they are produced by the workflow instances.
  • No notification should be lost when subscribing a user while notifying this same user. This should be done without locking the entire system.
  • Once completed, a notification should not be sent to users because its associated task does not exist anymore in Camunda. Such notification is not active anymore.

Data model

Here’s a representation of the table where notifications will be stored.

notification table

In our case, we are going to use the id of the Camunda task as business_id.

Now, we also need a table to store the recipients i.e the list of users or groups of users that should receive notification updates.

recipient table

Notice the type of the field is_read. It is a byte to be able to aggregate its values when querying (see below the Notification Service section). Furthermore, this field is only relevant for a user, not a group because we just need to know if the user has read the associated notification or not. There is a one-to-may relationship between the notification table and the recipient table.

So the idea is when a new task (each task has a list of user candidates and/or group candidates) is created by Camunda, a notification will be created and save into the notification table (one new row) and then a new row will be appended to the recipient table for each user candidate and group candidate.

For instance, in our case, when a new first task of the workflow is created, the tables will look like:

notification table
recipient table

I choose to use only one table here for the recipients instead of two (one for the users, one for groups) for the sake of simplicity.

Implementation

So now that we have our use case, our requirements and the data model, we can start implementing our different components. I won’t go to much into details (you can find the whole code in the Github repository) but let’s take a tour of the different components.

TaskListener

Camunda is very flexible. It provides many entry points to add your own logic during the execution of a workflow process.

A task listener is used to execute custom Java logic or an expression upon the occurrence of a certain task-related event.

That’s exactly what we need! In our simple case, we will just listen for CREATE and COMPLETE events and notify the subscribers.

The implementation will look like this:

The candidates are extracted from the attributes of the task and they corresponds to the users or groups to notify.

Notification service

The code of the interface and its implementation. It is the service that manages the notifications. It can:

  • create a notification and save it in a repository along with the recipients
  • tag/mark a notification ‘as read’
  • tag/mark a notification ‘as inactive’
  • retrieve unread and active notifications. They are given in descending order of created_at. Note the subtlety in the SQL query. The group by (because of the one-to-many relation) is done by notification id and the values of recipient field is_read are aggregated with the max function and compare to zero. If it’s higher than zero, it means the user tagged the notification ‘as read’
SELECT n.id, n.type, n.message, n.created_at, n.is_active, n.business_id FROM notification n 
INNER JOIN recipient r ON n.id = r.notification_id
WHERE (r.user_id = :userId OR r.group_id IN :groupIds) AND n.is_active = true
GROUP BY n.id HAVING max(r.is_read) = 0 ORDER BY n.created_at DESC

:userId and :groupIds are parameters set dynamically.

Registrar

The registrar is the component with which a subscriber can subscribe to notification intended to a given user, designated by a CamundaUserDetails instance. This object returns the user id and the group ids a user belongs to. Registration is done via this method.

Subscription subscribe(CamundaUserDetails userDetails, Subscriber<Notification> subscriber)

All unread notifications are sent to the subscriber during the registration and all new notifications will be transmitted afterwards. The return object, Subscription, is for unsubscribing purpose. The implementation is designed to support parallel subscriptions for different users and subscription can occur during notification publication (a striped read/write lock is used).

To help you going through the code and the different components, you can read the different test classes in that order: TestBasicApprovalWorkflow, TestNotificationService, TestApprovalWorkflowRegistrar and NotificationIntegrationTest.

Final words

The source code is available here. It is not intended to be used in production as it is. Some additional tests need to be performed. If a real database implementation is chosen (MySql, PostgreSQL…), some queries used in the Notification Service can be rewritten for better performance like the one to mark a notification ‘as read’ to update a joined table in one single query instead of two.

The code has been designed with the intent to subscribe via a single page application using WebSockets. Considering this is not the most challenging part it is not detailed here and left to the reader :)

--

--

Paul Bares
Paul Bares

Written by Paul Bares

I'm an enthusiast in computer hardware and programming. I specialize in high performance and parallel computing. Co-Creator of SquashQL Github: squashql