Context is everything

Utilizing a well-hidden-in-plain-sight workflow feature

"Workflow" illustration, CC-by 2.0 Dave Fayram, from

If you’ve ever looked at a Liferay workflow implementation and its scripts, you might have seen workflowContext being referenced in the scripts that are executed in the individual tasks and states.

I’ve recently had my first scripting contact with Workflow, and wanted to look at this context, and what it can do for me. Digging a bit, you’ll find out that workflowContext is a Map<String, Serializable> - interesting: Serializable hints at it being available in later steps of the workflow again, when filled in the beginning. And indeed, that’s the case.

Here’s a very simple example for how it can be useful:

First of all: The dynamic portion of a workflow is a mixture of Freemarker (for notifications) and Groovy (for scripts).

The default Single Approver Workflow sends mail without any subject. That’s ugly, but can be changed easily: Notifications have a Description and a Template. Their description turns into the email subject - and by default it’s empty. So you’ll just need to fill it: Static text is fine (“Please review a workflow submission”), but you can do better with workflowContext:

Before making the email more personal, we’ll have to go into scripting: Look at Single Approver’s initial state, “created”: It doesn’t have any action, but you can add one. Let’s make it extremely simple and just cater for JournalArticle (that's a Web Content Article on the UI - other types are left as an API exercise for the reader): onExit, enter this script:

import com.liferay.asset.kernel.model.AssetRenderer;
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.WorkflowStatusManagerUtil;

long classPK = GetterUtil.getLong((String)workflowContext.get(WorkflowConstants.CONTEXT_ENTRY_CLASS_PK));
String className = (String)workflowContext.get(WorkflowConstants.CONTEXT_ENTRY_CLASS_NAME);
WorkflowHandler workflowHandler = WorkflowHandlerRegistryUtil.getWorkflowHandler(className);
AssetRenderer assetRenderer = workflowHandler.getAssetRenderer(classPK);
String assetTitle = "none";
try {
  assetTitle = assetRenderer.getAssetObject().getTitle();
} catch ( java.lang.Exception e) {
  // ignore. Note: Above code works for JournalArticle, but
  // not every asset has a getTitle method. Those will fail, 
​​​​​​​  // but we ignore this in the quick sample here.
workflowContext.put("assetTitle", assetTitle);
WorkflowStatusManagerUtil.updateStatus(WorkflowConstants.getLabelStatus("pending"), workflowContext);

The magic words are the last two lines: From now on, any time in the workflow, we can reference workflowContext.get(“assetTitle”) in scripts, or ${assetTitle} in Freemarker-enabled fields.

Go ahead and change the description of this workflow’s “Review Notification” to “please review ${assetTitle}” and provide your reviewers with more meaningful notifications.

Extend this with anything you want to store in the workflowContext. Well, not anything - don't overdo it. But it can simplify your other workflow scripts tremendously, and provide personalized and meaningful notifications to your customers.

Need some icing on the cake?

Description is a single line and turns into the subject of an E-Mail, while Template can be multi-line and will be the body of the email (e.g. insert <br/> or other HTML markup). However, if you’re creating a User Notification, Template will be single line, with HTML tags escaped, and the only content shown (no Description) - it will be the title of the UI Notification. You might want to split up the current single notification into two, to cater for each of the channels individually.