Issue with `DelegateTask.getCandidates()`

Issue with DelegateTask.getCandidates() Not Reflecting Deleted Candidate Groups in TaskListener (Camunda 7)

Hi everyone,

We’ve developed a plugin for Camunda 7 that hooks into various TaskListener events. The listener is registered like this:

taskDefinition.addTaskListener(TaskListener.EVENTNAME_CREATE, globalTaskListener);
taskDefinition.addTaskListener(TaskListener.EVENTNAME_COMPLETE, globalTaskListener);
taskDefinition.addTaskListener(TaskListener.EVENTNAME_DELETE, globalTaskListener);
taskDefinition.addTaskListener(TaskListener.EVENTNAME_UPDATE, globalTaskListener);

Everything has been working smoothly. However, we recently started using identity links to manage candidate groups on UserTask.

The Problem

In our implementation, GlobalTaskListener implements TaskListener. The issue arises when we delete a candidate group from a task:

  • The TaskListener is correctly triggered on EVENTNAME_UPDATE.
  • But when we call delegateTask.getCandidates(), it still returns the candidate group that was just deleted.
  • Interestingly, when adding a candidate group, delegateTask.getCandidates() reflects the change immediately.

What I’ve Tried

  • I noticed that if I hook into the HistoryEventHandler, it properly captures the HistoryEventTypes.IDENTITY_LINK_DELETE event.
  • However, my goal is to rely on the TaskListener.EVENTNAME_UPDATE trigger and have the DelegateTask reflect the current, accurate state of identity links (i.e., candidate groups).

My Questions

  • Why isn’t delegateTask.getCandidates() up-to-date after deleting a candidate group?
  • Is this behavior by design in Camunda 7?
  • Could this be related to caching, or is it potentially a bug?
  • Is there a recommended approach to ensure that, within the TaskListener, I get the correct, updated list of candidates after such changes?

I’ve searched through the documentation and forums but couldn’t find a clear explanation for this behavior.

Any insights, explanations, or workarounds would be greatly appreciated!

Thanks in advance!

Hi @Brian954,

Why delegateTask.getCandidates() Still Shows Deleted Candidates

Camunda Internals

Camunda maintains identity links (e.g., candidate users/groups) via a separate persistence mechanism. When you update a task (e.g., delete a candidate group), this update is queued in the same transaction but not yet flushed to the database when the TaskListener fires.

  • TaskListener.EVENTNAME_UPDATE is triggered before the actual database update is committed.
  • As a result, delegateTask.getCandidates() still reflects the in-memory state before the identity link deletion is flushed.

So, this is expected behavior—though confusing.


Why Adding a Candidate Group Reflects Immediately

When you add a candidate group, it’s added to the delegateTask in-memory structure, so it appears immediately in getCandidates().

Removing a candidate, however, may not immediately update the internal structures that back getCandidates(), which are lazily fetched or cached.


Workarounds & Solutions

1. Use the IdentityLinkService or Custom Query

Instead of relying on delegateTask.getCandidates(), use a direct query to the runtime service to get the current identity links, like so:

List<IdentityLink> currentLinks = taskService.getIdentityLinksForTask(delegateTask.getId());

Then filter by IdentityLinkType.CANDIDATE and groupId != null.

List<String> candidateGroups = currentLinks.stream()
    .filter(link -> IdentityLinkType.CANDIDATE.equals(link.getType()) && link.getGroupId() != null)
    .map(IdentityLink::getGroupId)
    .collect(Collectors.toList());

This will reflect the most accurate in-memory state, including deletes that occurred earlier in the same transaction.


2. Defer Execution Until After Commit

If you absolutely need the final committed state, consider using a transaction listener or a history event handler.

For example:

context.getCommandContext().getTransactionContext()
    .addTransactionListener(TransactionState.COMMITTED, () -> {
        // safe to access fully committed DB state here
    });

Note: At this point, you can use a custom query to fetch the post-commit identity link state from the database if needed.


3. Is This a Bug or by Design?

This is by design, though it’s arguably a design limitation.

  • Camunda 7 operates on a deferred commit model, meaning many operations within a transaction aren’t visible until the commit phase.
  • TaskListener operates within the transaction, not after it.
  • delegateTask.getCandidates() is not guaranteed to be fresh; it’s a snapshot of the current in-memory state which may be stale.

Recommended Approach

For accuracy in listeners:

@Override
public void notify(DelegateTask delegateTask) {
    List<IdentityLink> links = delegateTask.getProcessEngineServices()
        .getTaskService()
        .getIdentityLinksForTask(delegateTask.getId());

    List<String> candidateGroups = links.stream()
        .filter(link -> IdentityLinkType.CANDIDATE.equals(link.getType()) && link.getGroupId() != null)
        .map(IdentityLink::getGroupId)
        .collect(Collectors.toList());

    // use candidateGroups instead of delegateTask.getCandidates()
}
1 Like

@Brian954 , Here’s how to register your GlobalTaskListener in Camunda 7 Spring Boot using a plugin-style configuration.


1. GlobalTaskListener Implementation

package com.example.camunda.plugin;

import org.camunda.bpm.engine.delegate.DelegateTask;
import org.camunda.bpm.engine.delegate.TaskListener;
import org.camunda.bpm.engine.task.IdentityLink;
import org.camunda.bpm.engine.task.IdentityLinkType;

import java.util.List;
import java.util.stream.Collectors;

public class GlobalTaskListener implements TaskListener {

    @Override
    public void notify(DelegateTask delegateTask) {
        List<IdentityLink> identityLinks = delegateTask.getProcessEngineServices()
            .getTaskService()
            .getIdentityLinksForTask(delegateTask.getId());

        List<String> candidateGroups = identityLinks.stream()
            .filter(link -> IdentityLinkType.CANDIDATE.equals(link.getType()) && link.getGroupId() != null)
            .map(IdentityLink::getGroupId)
            .collect(Collectors.toList());

        System.out.println("Accurate Candidate Groups (from TaskService): " + candidateGroups);

        // Add your plugin logic here
    }
}

2. Spring Boot Configuration for Plugin Registration

package com.example.camunda.config;

import com.example.camunda.plugin.GlobalTaskListener;
import org.camunda.bpm.engine.impl.bpmn.parser.AbstractBpmnParseListener;
import org.camunda.bpm.engine.impl.bpmn.parser.BpmnParseListener;
import org.camunda.bpm.engine.impl.persistence.entity.TaskDefinition;
import org.camunda.bpm.engine.impl.task.UserTaskActivityBehavior;
import org.camunda.bpm.engine.impl.util.xml.Element;
import org.camunda.bpm.engine.impl.pvm.process.ActivityImpl;
import org.camunda.bpm.engine.impl.pvm.process.ScopeImpl;
import org.camunda.bpm.spring.boot.starter.configuration.impl.DefaultCamundaBpmConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class CamundaPluginConfig extends DefaultCamundaBpmConfiguration {

    @Bean
    public BpmnParseListener globalTaskListenerParseListener() {
        return new AbstractBpmnParseListener() {
            private final GlobalTaskListener listener = new GlobalTaskListener();

            @Override
            public void parseUserTask(Element userTaskElement, ScopeImpl scope, ActivityImpl activity) {
                UserTaskActivityBehavior behavior = (UserTaskActivityBehavior) activity.getActivityBehavior();
                TaskDefinition taskDefinition = behavior.getTaskDefinition();

                taskDefinition.addTaskListener("create", listener);
                taskDefinition.addTaskListener("update", listener);
                taskDefinition.addTaskListener("complete", listener);
                taskDefinition.addTaskListener("delete", listener);
            }
        };
    }
}

**3. With this config: **

  • Your GlobalTaskListener will be automatically applied to all user tasks during BPMN parse time.
  • It will log or act upon the most accurate list of candidate groups, even after deletions.
  • You avoid stale state from delegateTask.getCandidates().

I tried it similar but sadly that doenst seem to work like expected but maybe im doing something wrong.

My test scenario was:

  1. Add Group1
  2. Add Group2
  3. Delete Group2
  4. Delete Group1

The code that creates the logs is this:

Log code
	log.info("Found candidateGroups " + delegateTask.getCandidates().size() + " candidateGroups links for task: " + tsk.id);
		for (IdentityLink link : delegateTask.getCandidates()) {
			log.info("Identity Link - Type: " + link.getType() + 
						", User ID: " + link.getUserId() + 
						", Group ID: " + link.getGroupId() + 
						", Task ID: " + link.getTaskId());
		}
		
		List<IdentityLinkEntity> links = Context
          .getCommandContext()
          .getIdentityLinkManager()
          .findIdentityLinksByTaskId(tsk.id);
		
		log.info("----> NEW Found " + links.size() + " identity links for task: " + tsk.id);
		for (IdentityLink link : links) {
			log.info("Identity Link - Type: " + link.getType() + 
						", User ID: " + link.getUserId() + 
						", Group ID: " + link.getGroupId() + 
						", Task ID: " + link.getTaskId());
		}

	    List<IdentityLink> linksTask = delegateTask.getProcessEngineServices()
	            .getTaskService()
	            .getIdentityLinksForTask(delegateTask.getId());
				
				log.info("------> Task Found links " + linksTask.size() + " identity links for task: " + tsk.id);
				for (IdentityLink link : linksTask) {
					log.info("Identity Link - Type: " + link.getType() + 
								", User ID: " + link.getUserId() + 
								", Group ID: " + link.getGroupId() + 
								", Task ID: " + link.getTaskId());
				}
Logs

2025-04-25T06:55:32.249Z INFO 56 — [nio-8443-exec-1] o.c.b.fincent.plugin.GlobalTaskListener : Found candidateGroups 1 candidateGroups links for task: 17ca5ae1-20f1-11f0-988c-ae98ea46b75b
2025-04-25T06:55:32.250Z INFO 56 — [nio-8443-exec-1] o.c.b.fincent.plugin.GlobalTaskListener : Identity Link - Type: candidate, User ID: null, Group ID: Group1, Task ID: 17ca5ae1-20f1-11f0-988c-ae98ea46b75b
2025-04-25T06:55:32.250Z INFO 56 — [nio-8443-exec-1] o.c.b.fincent.plugin.GlobalTaskListener : ----> NEW Found 1 identity links for task: 17ca5ae1-20f1-11f0-988c-ae98ea46b75b
2025-04-25T06:55:32.250Z INFO 56 — [nio-8443-exec-1] o.c.b.fincent.plugin.GlobalTaskListener : Identity Link - Type: candidate, User ID: null, Group ID: Group1, Task ID: 17ca5ae1-20f1-11f0-988c-ae98ea46b75b
2025-04-25T06:55:32.250Z INFO 56 — [nio-8443-exec-1] o.c.b.fincent.plugin.GlobalTaskListener : ------> Task Found links 2 identity links for task: 17ca5ae1-20f1-11f0-988c-ae98ea46b75b
2025-04-25T06:55:32.250Z INFO 56 — [nio-8443-exec-1] o.c.b.fincent.plugin.GlobalTaskListener : Identity Link - Type: candidate, User ID: null, Group ID: Group1, Task ID: 17ca5ae1-20f1-11f0-988c-ae98ea46b75b
2025-04-25T06:55:32.251Z INFO 56 — [nio-8443-exec-1] o.c.b.fincent.plugin.GlobalTaskListener : Identity Link - Type: assignee, User ID: 03bf51d0-d465-42f2-99e7-95edd4251963, Group ID: null, Task ID: 17ca5ae1-20f1-11f0-988c-ae98ea46b75b
2025-04-25T06:55:46.912Z INFO 56 — [nio-8443-exec-3] o.c.b.fincent.plugin.GlobalTaskListener : Found candidateGroups 2 candidateGroups links for task: 17ca5ae1-20f1-11f0-988c-ae98ea46b75b
2025-04-25T06:55:46.912Z INFO 56 — [nio-8443-exec-3] o.c.b.fincent.plugin.GlobalTaskListener : Identity Link - Type: candidate, User ID: null, Group ID: Group1, Task ID: 17ca5ae1-20f1-11f0-988c-ae98ea46b75b
2025-04-25T06:55:46.912Z INFO 56 — [nio-8443-exec-3] o.c.b.fincent.plugin.GlobalTaskListener : Identity Link - Type: candidate, User ID: null, Group ID: Group2, Task ID: 17ca5ae1-20f1-11f0-988c-ae98ea46b75b
2025-04-25T06:55:46.913Z INFO 56 — [nio-8443-exec-3] o.c.b.fincent.plugin.GlobalTaskListener : ----> NEW Found 1 identity links for task: 17ca5ae1-20f1-11f0-988c-ae98ea46b75b
2025-04-25T06:55:46.913Z INFO 56 — [nio-8443-exec-3] o.c.b.fincent.plugin.GlobalTaskListener : Identity Link - Type: candidate, User ID: null, Group ID: Group1, Task ID: 17ca5ae1-20f1-11f0-988c-ae98ea46b75b
2025-04-25T06:55:46.913Z INFO 56 — [nio-8443-exec-3] o.c.b.fincent.plugin.GlobalTaskListener : ------> Task Found links 3 identity links for task: 17ca5ae1-20f1-11f0-988c-ae98ea46b75b
2025-04-25T06:55:46.913Z INFO 56 — [nio-8443-exec-3] o.c.b.fincent.plugin.GlobalTaskListener : Identity Link - Type: candidate, User ID: null, Group ID: Group1, Task ID: 17ca5ae1-20f1-11f0-988c-ae98ea46b75b
2025-04-25T06:55:46.913Z INFO 56 — [nio-8443-exec-3] o.c.b.fincent.plugin.GlobalTaskListener : Identity Link - Type: candidate, User ID: null, Group ID: Group2, Task ID: 17ca5ae1-20f1-11f0-988c-ae98ea46b75b
2025-04-25T06:55:46.913Z INFO 56 — [nio-8443-exec-3] o.c.b.fincent.plugin.GlobalTaskListener : Identity Link - Type: assignee, User ID: 03bf51d0-d465-42f2-99e7-95edd4251963, Group ID: null, Task ID: 17ca5ae1-20f1-11f0-988c-ae98ea46b75b
2025-04-25T06:55:52.419Z INFO 56 — [nio-8443-exec-6] o.c.b.f.plugin.InnoHistoryEventHandler : LOG LINK HISTORY EVENT: TaskId17ca5ae1-20f1-11f0-988c-ae98ea46b75b GroupId Group2
2025-04-25T06:55:52.429Z INFO 56 — [nio-8443-exec-6] o.c.b.fincent.plugin.GlobalTaskListener : Found candidateGroups 2 candidateGroups links for task: 17ca5ae1-20f1-11f0-988c-ae98ea46b75b
2025-04-25T06:55:52.430Z INFO 56 — [nio-8443-exec-6] o.c.b.fincent.plugin.GlobalTaskListener : Identity Link - Type: candidate, User ID: null, Group ID: Group1, Task ID: 17ca5ae1-20f1-11f0-988c-ae98ea46b75b
2025-04-25T06:55:52.430Z INFO 56 — [nio-8443-exec-6] o.c.b.fincent.plugin.GlobalTaskListener : Identity Link - Type: candidate, User ID: null, Group ID: Group2, Task ID: 17ca5ae1-20f1-11f0-988c-ae98ea46b75b
2025-04-25T06:55:52.430Z INFO 56 — [nio-8443-exec-6] o.c.b.fincent.plugin.GlobalTaskListener : ----> NEW Found 2 identity links for task: 17ca5ae1-20f1-11f0-988c-ae98ea46b75b
2025-04-25T06:55:52.430Z INFO 56 — [nio-8443-exec-6] o.c.b.fincent.plugin.GlobalTaskListener : Identity Link - Type: candidate, User ID: null, Group ID: Group1, Task ID: 17ca5ae1-20f1-11f0-988c-ae98ea46b75b
2025-04-25T06:55:52.430Z INFO 56 — [nio-8443-exec-6] o.c.b.fincent.plugin.GlobalTaskListener : Identity Link - Type: candidate, User ID: null, Group ID: Group2, Task ID: 17ca5ae1-20f1-11f0-988c-ae98ea46b75b
2025-04-25T06:55:52.431Z INFO 56 — [nio-8443-exec-6] o.c.b.fincent.plugin.GlobalTaskListener : ------> Task Found links 3 identity links for task: 17ca5ae1-20f1-11f0-988c-ae98ea46b75b
2025-04-25T06:55:52.431Z INFO 56 — [nio-8443-exec-6] o.c.b.fincent.plugin.GlobalTaskListener : Identity Link - Type: candidate, User ID: null, Group ID: Group1, Task ID: 17ca5ae1-20f1-11f0-988c-ae98ea46b75b
2025-04-25T06:55:52.431Z INFO 56 — [nio-8443-exec-6] o.c.b.fincent.plugin.GlobalTaskListener : Identity Link - Type: candidate, User ID: null, Group ID: Group2, Task ID: 17ca5ae1-20f1-11f0-988c-ae98ea46b75b
2025-04-25T06:55:52.431Z INFO 56 — [nio-8443-exec-6] o.c.b.fincent.plugin.GlobalTaskListener : Identity Link - Type: assignee, User ID: 03bf51d0-d465-42f2-99e7-95edd4251963, Group ID: null, Task ID: 17ca5ae1-20f1-11f0-988c-ae98ea46b75b
2025-04-25T06:55:55.447Z INFO 56 — [nio-8443-exec-6] o.c.b.f.plugin.InnoHistoryEventHandler : LOG LINK HISTORY EVENT: TaskId17ca5ae1-20f1-11f0-988c-ae98ea46b75b GroupId Group1
2025-04-25T06:55:55.457Z INFO 56 — [nio-8443-exec-6] o.c.b.fincent.plugin.GlobalTaskListener : Found candidateGroups 1 candidateGroups links for task: 17ca5ae1-20f1-11f0-988c-ae98ea46b75b
2025-04-25T06:55:55.458Z INFO 56 — [nio-8443-exec-6] o.c.b.fincent.plugin.GlobalTaskListener : Identity Link - Type: candidate, User ID: null, Group ID: Group1, Task ID: 17ca5ae1-20f1-11f0-988c-ae98ea46b75b
2025-04-25T06:55:55.458Z INFO 56 — [nio-8443-exec-6] o.c.b.fincent.plugin.GlobalTaskListener : ----> NEW Found 1 identity links for task: 17ca5ae1-20f1-11f0-988c-ae98ea46b75b
2025-04-25T06:55:55.458Z INFO 56 — [nio-8443-exec-6] o.c.b.fincent.plugin.GlobalTaskListener : Identity Link - Type: candidate, User ID: null, Group ID: Group1, Task ID: 17ca5ae1-20f1-11f0-988c-ae98ea46b75b
2025-04-25T06:55:55.459Z INFO 56 — [nio-8443-exec-6] o.c.b.fincent.plugin.GlobalTaskListener : ------> Task Found links 2 identity links for task: 17ca5ae1-20f1-11f0-988c-ae98ea46b75b
2025-04-25T06:55:55.459Z INFO 56 — [nio-8443-exec-6] o.c.b.fincent.plugin.GlobalTaskListener : Identity Link - Type: candidate, User ID: null, Group ID: Group1, Task ID: 17ca5ae1-20f1-11f0-988c-ae98ea46b75b
2025-04-25T06:55:55.459Z INFO 56 — [nio-8443-exec-6] o.c.b.fincent.plugin.GlobalTaskListener : Identity Link - Type: assignee, User ID: 03bf51d0-d465-42f2-99e7-95edd4251963, Group ID: null, Task ID: 17ca5ae1-20f1-11f0-988c-ae98ea46b75b

Is there another service i can use / call?