Workflow Scripting: Programmatic Task Assignment

This content is meant to be added as a use case to the 7.2 Workflow Scripting official documentation, but isn't ready to go through the full process an official docs article passes through.

In the Category Specific Definition, there’s a script in a condition node with logic for determining whether to proceed to one transition or another, based on whether the asset is categorized as legal or not.

returnValue = "Content Review";

for (AssetCategory assetCategory : assetCategories) {
    String categoryName = assetCategory.getName();

    if (categoryName.equals("legal")) {
        returnValue = "Legal Review";

        return;
    }
}

Imagine you needed to check an asset category like this, but instead of only providing one alternate path if the asset is categorized as legal, you have a dozen different tasks to send processing to depending on the asset category? You could make a dozen workflow tasks with the corresponding transitions and code the condition to got to each transition based on the asset category, and that’s a fine solution. But drawing that workflow diagram would be a nightmare, and it would look more complex than it really is. Instead, you can hide that faux complexity by programmatically changing the assignment of the task based on the asset category. This diagram could look identical to the Single Approver definition, with logic added in a script to the review task, dynamically assigning the task to a different User or Role.

If you’re already armed with the Category Specific Definition, you can see the logic for checking the categories. The only piece you’re missing is how to make the assignments. Use the WorkflowTaskManager methods for this:

  • WorkflowTaskManager.assignWorkflowTaskToUser assigns the task to a User by the userId.
  • WorkflowTaskManager.assignWorkflowTaskToRole assigns the task to a Role by the roleId.

Both methods require a long workflowTaskId. Retrieve this from the injected kaleoTaskInstanceToken script variable. java workflowTaskId = kaleoTaskInstanceToken.getKaleoTaskInstanceTokenId();

The script snippet for this looks like this:

import com.liferay.asset.kernel.model.AssetCategory;
import com.liferay.asset.kernel.model.AssetEntry;
import com.liferay.asset.kernel.model.AssetRenderer;
import com.liferay.asset.kernel.model.AssetRendererFactory;
import com.liferay.asset.kernel.service.AssetEntryLocalServiceUtil;
import com.liferay.portal.kernel.util.GetterUtil;
import com.liferay.portal.kernel.workflow.WorkflowConstants;
import com.liferay.portal.kernel.workflow.WorkflowHandler;
import com.liferay.portal.kernel.workflow.WorkflowHandlerRegistryUtil;
import com.liferay.portal.kernel.workflow.WorkflowTaskManagerUtil;
import com.liferay.portal.kernel.model.Role;
import com.liferay.portal.kernel.service.RoleLocalServiceUtil;
import java.util.List;
import com.liferay.portal.kernel.model.User;
import com.liferay.portal.kernel.service.UserLocalServiceUtil;

String className = (String)workflowContext.get(WorkflowConstants.CONTEXT_ENTRY_CLASS_NAME);

WorkflowHandler workflowHandler = WorkflowHandlerRegistryUtil.getWorkflowHandler(className);

AssetRendererFactory assetRendererFactory = workflowHandler.getAssetRendererFactory();

long classPK = GetterUtil.getLong((String)workflowContext.get(WorkflowConstants.CONTEXT_ENTRY_CLASS_PK));

AssetRenderer assetRenderer = workflowHandler.getAssetRenderer(classPK);

AssetEntry assetEntry = assetRendererFactory.getAssetEntry(assetRendererFactory.getClassName(), assetRenderer.getClassPK());

List<AssetCategory> assetCategories = assetEntry.getCategories();

// Here is where this script differs from the category specific definition's
// although the loop is pretty much the same

for (AssetCategory assetCategory : assetCategories) {

    long companyId = GetterUtil.getLong((String) workflowContext.get(WorkflowConstants.CONTEXT_COMPANY_ID));

    long workflowTaskId = kaleoTaskInstanceToken.getKaleoTaskInstanceTokenId();

    Date dueDate = new Date();

    String categoryName = assetCategory.getName();

    String comment = "Please review the thing, would you? Its category is in your wheelhouse.";

    if (categoryName.equals("adventure")) {

        User adventureAssignee = UserLocalServiceUtil.getUserByEmailAddress(companyId, "ziltoid@lunarresort.com");

        WorkflowTaskManagerUtil.assignWorkflowTaskToUser(companyId, userId, workflowTaskId, adventureAssignee.getUserId(), comment, dueDate, workflowContext);
    }

    if (categoryName.equals("fixit")) {

        User fixitAssignee = UserLocalServiceUtil.getUserByEmailAddress(companyId, "james@lunarresort.com");

        WorkflowTaskManagerUtil.assignWorkflowTaskToUser(companyId, userId, workflowTaskId, fixitAssignee.getUserId(), comment, dueDate, workflowContext);
    }

    if (categoryName.equals("eatit")) {

        User eatitAssignee = UserLocalServiceUtil.getUserByEmailAddress(companyId, "marvin@lunarresort.com");

        WorkflowTaskManagerUtil.assignWorkflowTaskToUser(companyId, userId, workflowTaskId, eatitAssignee.getUserId(), comment, dueDate, workflowContext);
    }

    else {

        Role assigneeRole = RoleLocalServiceUtil.getRole(companyId, "Portal Content Reviewer");
        long roleId = assigneeRole.getRoleId();

        WorkflowTaskManagerUtil.assignWorkflowTaskToRole(companyId, userId, workflowTaskId, roleId, comment, dueDate, workflowContext);
    }
}

Here are the relevant method signatures from WorkflowTaskManager:

public WorkflowTask assignWorkflowTaskToRole(
        long companyId, long userId, long workflowTaskId, long roleId,
        String comment, Date dueDate,
        Map<String, Serializable> workflowContext)
    throws WorkflowException;

public WorkflowTask assignWorkflowTaskToUser(
        long companyId, long userId, long workflowTaskId,
        long assigneeUserId, String comment, Date dueDate,
        Map<String, Serializable> workflowContext)
    throws WorkflowException;

This represents just one example of what you can do with access to the WorkflowTaskManager methods.

A Full Workflow Definition, Including the Conditional Assignment Script

To test this, you’ll need to create the three users whose emails are hardcoded in the script:

Make sure a User has the Portal Content Reviewer role as well, since the logic sends the workflow task to that role if one of these categories aren’t added to the asset being run through the workflow: eatit, adventure, fixit

Without further ado, here’s the XML

<?xml version="1.0"?>
<workflow-definition
	xmlns="urn:liferay.com:liferay-workflow_7.2.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:liferay.com:liferay-workflow_7.2.0 http://www.liferay.com/dtd/liferay-workflow-definition_7_2_0.xsd">
	<name>Conditional Scripted Single Approver</name>
	<description>A single approver can approve a workflow content.</description>
	<version>1</version>
	<state>
		<name>created</name>
		<metadata>
			<![CDATA[{"xy":[36,51]}]]>
		</metadata>
		<initial>true</initial>
		<transitions>
			<transition>
				<name>review</name>
				<target>review</target>
				<default>true</default>
			</transition>
		</transitions>
	</state>
	<state>
		<name>approved</name>
		<metadata>
			<![CDATA[{"xy":[380,51]}]]>
		</metadata>
		<actions>
			<action>
				<name>approve</name>
				<script>
					<![CDATA[import com.liferay.portal.kernel.workflow.WorkflowStatusManagerUtil;
						import com.liferay.portal.kernel.workflow.WorkflowConstants;

						WorkflowStatusManagerUtil.updateStatus(WorkflowConstants.getLabelStatus("approved"), workflowContext);]]>
				</script>
				<script-language>groovy</script-language>
				<execution-type>onEntry</execution-type>
			</action>
		</actions>
	</state>
	<task>
		<name>update</name>
		<metadata>
			<![CDATA[{"transitions":{"resubmit":{"bendpoints":[[303,140]]}},"xy":[328,199]}]]>
		</metadata>
		<actions>
			<notification>
				<name>Creator Modification Notification</name>
				<template>
					<![CDATA[Your submission was rejected by ${userName}, please modify and resubmit.]]>
				</template>
				<template-language>freemarker</template-language>
				<notification-type>email</notification-type>
				<notification-type>user-notification</notification-type>
				<recipients receptionType="to">
					<user/>
				</recipients>
				<execution-type>onAssignment</execution-type>
			</notification>
		</actions>
		<assignments>
			<user/>
		</assignments>
		<transitions>
			<transition>
				<name>resubmit</name>
				<target>review</target>
				<default>true</default>
			</transition>
		</transitions>
	</task>
	<task>
		<name>review</name>
		<metadata>
			<![CDATA[{"xy":[168,36]}]]>
		</metadata>
		<actions>
			<action>
				<name>assign</name>
				<script>
					<![CDATA[import com.liferay.asset.kernel.model.AssetCategory;
import com.liferay.asset.kernel.model.AssetEntry;
import com.liferay.asset.kernel.model.AssetRenderer;
import com.liferay.asset.kernel.model.AssetRendererFactory;
import com.liferay.asset.kernel.service.AssetEntryLocalServiceUtil;
import com.liferay.portal.kernel.util.GetterUtil;
import com.liferay.portal.kernel.workflow.WorkflowConstants;
import com.liferay.portal.kernel.workflow.WorkflowHandler;
import com.liferay.portal.kernel.workflow.WorkflowHandlerRegistryUtil;
import com.liferay.portal.kernel.workflow.WorkflowTaskManager;
import com.liferay.portal.kernel.workflow.WorkflowTaskManagerUtil;
import com.liferay.portal.kernel.model.Role;
import com.liferay.portal.kernel.service.RoleLocalServiceUtil;
import java.util.List;
import com.liferay.portal.kernel.model.User;
import com.liferay.portal.kernel.service.UserLocalServiceUtil;

String className = (String)workflowContext.get(WorkflowConstants.CONTEXT_ENTRY_CLASS_NAME);

WorkflowHandler workflowHandler = WorkflowHandlerRegistryUtil.getWorkflowHandler(className);

AssetRendererFactory assetRendererFactory = workflowHandler.getAssetRendererFactory();

long classPK = GetterUtil.getLong((String)workflowContext.get(WorkflowConstants.CONTEXT_ENTRY_CLASS_PK));

AssetRenderer assetRenderer = workflowHandler.getAssetRenderer(classPK);

AssetEntry assetEntry = assetRendererFactory.getAssetEntry(assetRendererFactory.getClassName(), assetRenderer.getClassPK());

List<AssetCategory> assetCategories = assetEntry.getCategories();

// Here is where this script differs from the category specific definition's
// although the loop is pretty much the same

for (AssetCategory assetCategory : assetCategories) {

    long companyId = GetterUtil.getLong((String) workflowContext.get(WorkflowConstants.CONTEXT_COMPANY_ID));

    long workflowTaskId = kaleoTaskInstanceToken.getKaleoTaskInstanceTokenId();

    Date dueDate = new Date();

    String categoryName = assetCategory.getName();

    String comment = "Please review the thing, would you? Its category is in your wheelhouse.";

    if (categoryName.equals("adventure")) {

        User adventureAssignee = UserLocalServiceUtil.getUserByEmailAddress(companyId, "ziltoid@lunarresort.com");

        WorkflowTaskManagerUtil.assignWorkflowTaskToUser(companyId, userId, workflowTaskId, adventureAssignee.getUserId(), comment, dueDate, workflowContext);
    }

    if (categoryName.equals("fixit")) {

        User fixitAssignee = UserLocalServiceUtil.getUserByEmailAddress(companyId, "james@lunarresort.com");

        WorkflowTaskManagerUtil.assignWorkflowTaskToUser(companyId, userId, workflowTaskId, fixitAssignee.getUserId(), comment, dueDate, workflowContext);
    }

    if (categoryName.equals("eatit")) {

        User eatitAssignee = UserLocalServiceUtil.getUserByEmailAddress(companyId, "marvin@lunarresort.com");

        WorkflowTaskManagerUtil.assignWorkflowTaskToUser(companyId, userId, workflowTaskId, eatitAssignee.getUserId(), comment, dueDate, workflowContext);
    }

    else {

        Role assigneeRole = RoleLocalServiceUtil.getRole(companyId, "Portal Content Reviewer");
        long roleId = assigneeRole.getRoleId();

        WorkflowTaskManagerUtil.assignWorkflowTaskToRole(companyId, userId, workflowTaskId, roleId, comment, dueDate, workflowContext);
    }
}]]>
				</script>
				<script-language>groovy</script-language>
				<execution-type>onEntry</execution-type>
			</action>
			<notification>
				<name>Review Completion Notification</name>
				<template>
					<![CDATA[Your submission was reviewed
					
					
					<#if taskComments?has_content> and the reviewer applied the following ${taskComments}</#if>.]]>
				</template>
				<template-language>freemarker</template-language>
				<notification-type>email</notification-type>
				<recipients receptionType="to">
					<user/>
				</recipients>
				<execution-type>onExit</execution-type>
			</notification>
			<notification>
				<name>Review Notification</name>
				<template>
					<![CDATA[${userName} sent you a ${entryType} for review in the workflow.]]>
				</template>
				<template-language>freemarker</template-language>
				<notification-type>email</notification-type>
				<notification-type>user-notification</notification-type>
				<recipients receptionType="to">
					<user/>
				</recipients>
				<execution-type>onAssignment</execution-type>
			</notification>
		</actions>
		<assignments>
			<user/>
		</assignments>
		<transitions>
			<transition>
				<name>approve</name>
				<target>approved</target>
				<default>true</default>
			</transition>
			<transition>
				<name>reject</name>
				<target>update</target>
				<default>false</default>
			</transition>
		</transitions>
	</task>
</workflow-definition>

Known Issues

I noticed that, while I can make the assignment using this, the asset gets “stuck” in the workflow tasks portlet as being the Review task, even after it’s actually been approved. I'll try to figure out why this is the case, but the primary purpose of this exercise was to execute the script successfully and showcase the WorkflowTaskManager methods.

Blogs

Hi Russel, I think that's a better choiche move your code into a Java class instead of Groovy script for debug purpose. Do you agree?

Hi Enrico,

So, a disclaimer first. I'm not now, nor have I ever been, a "real" developer. I write documentation, so my expertise is somewhat limited. But here's my answer for what it's worth:

In general, I find writing scripts, and particularly workflow scripts, very frustrating, for the very reason you pointed out: debugging. It's much harder to track down errors when much of the time you only find them when you re-publish the workflow and have to do runtime testing. So yes, I do think you're correct. If you can complete the same task in a Java class, do it.

Now, creating workflows programmatically isn't something I'm familiar with, and I'm not sure if there's a way to extract a script's logic easily into a Java class/module deployed to Liferay. Perhaps you've done this?

Yes, look here (sorry for formatting):

 

[...]

<assignments>

  <scripted-assignment>

    <script><![CDATA[

 

import com.liferay.portal.template.ServiceLocator;

import it.intesys.workflow.util.WorkflowUtil;

 

ServiceLocator serviceLocator = ServiceLocator.getInstance();

 

WorkflowUtil workflowUtil = (WorkflowUtil)      serviceLocator.findService("it.intesys.workflow.util.WorkflowUtil");

roles = workflowUtil.getAssigneRolesWorkflowTask(Long.valueOf(companyId),    Long.valueOf(groupId), entryClassName, Long.valueOf(entryClassPK),

Long.valueOf(userId));

 

user=null;

 

]]>

 

      </script>

    <script-language>groovy</script-language>

  </scripted-assignment>

</assignments>

 

The method "workflowUtil.getAssigneRolesWorkflowTask" (which returns a list roles) offers hospitality to your business logic written in Java so it can be debugged. 

 

About test running to check workflow changes I am not able to help you, I think there'isnt any possibities to do it now.