The Substitute - a holiday replacement approach using camunda candidate groups

In a recent project I was faced by a real classic in user task management. The age old question about how to handle holiday replacement the right way.

In this post I am going to explain my approach using as much of camunda ready-to-use functionality to avoid overhead.

Situation: Jack needs holidays and leaves for a week. John, the substitute, jumps in. He now has to deal with all of Jacks existing and newly created tasks.

A first approach often presented by developers is to postpone user assignment to the very last second before a user task is created. This can be done by some piece of code e.g. asking ldap for help.

But what about tasks already assigned to Jack? How can John get them to show up in his task list? The above mentioned solution does not work here.
The solution I was thinking of had to handle the following requirements:

1. assign new user tasks to the substitute collegue(s)
2. assign already existing user tasks to the substitute collegue(s)
3. holiday jack still can access all of his tasks (including new ones)
4. do not iterate over every single task and update the assignee

This is what I did: Instead of filling the assignee field of a user task during process modelling I made use of candidate groups. A candidate group contains all the users that have the right to work on a task.


For simplicity, lets call the candidate group "jacks_tasks". You can create it in camunda cockpit or using camunda's identity service at runtime. Create a user Jack and add him to his own candidate group.
Something like this (using IdentityService):

Group jacksGroup = identityService.newGroup("jacks_tasks");
identityService.saveGroup(jacksGroup);

User jack = identityService.newUser("Jack");
identityService.saveUser(jack);

identityService.createMembership(jack, jacksGroup);

I used a listener on task creation which ran the code in order to set up group and user straight before assignment.

To make sure Jack gets his task list filled (while not on holidays) you don't query for him as assignee but task candidate user (even the effort of refactoring this in an existing project isn't that bad):

List<Task> list = taskService.createTaskQuery().taskCandidateUser("Jack").list();

Note: the user Jack here is just a simple camunda user even without permission to log into camunda task list. Use any (external) user id you prefer and/or need in order to retrieve usertasks.

Now comes the substitution part

Create (even on the fly) a user John, add him to the candidate group jacks_tasks and John will have access to all of Jacks tasks immediately with this single configuration step.

User john = identityService.newUser("John");
identityService.saveUser(john);
identityService.createMembership(john, jacksGroup);

You don't need to iterate over dozens of tasks and update the assignee! Add more users to the group if John needs backup. Jack (member of the group) still can access his tasks. Nice and shiney.

Further thoughts:

  1. Yes I know, it is not very likely that Jack just leaves the office without notice. But think about a scenario where he calls in sick... But still, what about a foreseeable absence coming up? For that purpose we built a self-service for Jack and his collegues to configure substitution for a certain timespan. A util process takes care of the rest:  
  2. To prevent the camunda workflow engine from containing tons of generated users without any task (and hence purpose), you could clean up users and candidate groups in intervals/at process end. Take a look at this:
//get all users in jacks_tasks
List<User> userList = identityService.createUserQuery().memberOfGroup("jacks_tasks").list();

for (User user : userList) {
  List<Task> list = taskService.createTaskQuery().taskCandidateUser(user.getId()).list();
  //delete user if he has not tasks atm
  if(list.size() == 0){
    identityService.deleteUser(user.getId());
  }
}

List<Task> list = taskService.createTaskQuery().taskCandidateGroup("jacks_tasks").list();
//delete group if it has no tasks atm
if(list.size() == 0){
  identityService.deleteGroup(groupId);
}

check out and run the code

https://github.com/stefanfrena/camunda/tree/master/holiday-replacement

This approach works fine and I think it is a fine base for further requirements in user task management. Let me know what you think and if I can help you to understand this better. This was just a rough nutshell.