5.3 AEM Workflows, Custom Process Steps & Launchers
Key Takeaways
- The Granite Workflow engine separates design-time models under /conf from compiled runtime execution models synchronized to /var/workflow/models.
- Custom Java process steps implement com.adobe.granite.workflow.exec.WorkflowProcess, receiving WorkItem, WorkflowSession, and MetaDataMap to process payloads programmatically.
- Workflow payloads must differentiate between JCR_PATH (direct repository path string) and JCR_UUID (node UUID requiring session resolution) to prevent NullPointerException failures.
- Workflow Launchers automate executions based on Created, Modified, or Removed events, requiring strict path globs and property exclude lists to avoid fatal recursive infinite loops.
- Granite Workflow Purge Scheduler (com.adobe.granite.workflow.purge.Scheduler) must be configured to routinely clean completed and aborted instances under /var/workflow/instances to prevent repository degradation.
5.3 AEM Workflows, Custom Process Steps & Launchers
Exam Focus: The AEM Workflow framework automates content review, approval, metadata extraction, and activation governance. The AD0-E128 exam tests the Granite Workflow architecture (
com.adobe.granite.workflow), design-time models (/conf) vs. runtime models (/var/workflow/models), step types (Participant, Dialog Participant, Process Step, OR Split, AND Split), implementing custom Java process steps usingWorkflowProcess, safely resolvingJCR_PATHvs.JCR_UUIDpayload types, passing metadata arguments viaMetaDataMap, preventing infinite launcher loops onModifiedevents, and repository maintenance via the Workflow Purge Scheduler.
AEM Workflow Engine Architecture (Granite Workflow)
AEM Workflows are executed by the Granite Workflow Engine (com.adobe.granite.workflow). The framework is architected around a strict separation between model authoring, runtime deployment, and execution tracking.
1. Design-Time Models vs. Runtime Models
In modern AEM (and AEM as a Cloud Service), workflow models are authored and stored under configuration paths:
- Design-Time Model Path:
/conf/global/settings/workflow/models/<model-name>(or within project-specific tenant folders like/conf/my-app/settings/workflow/models). - Runtime Execution Path:
/var/workflow/models/<model-name>.
When a developer or workflow administrator edits a model in the Workflow Model Editor and clicks Sync, AEM validates the workflow graph, compiles its transitions, and synchronizes the executable definition into /var/workflow/models. The Granite workflow execution engine only executes models from /var/workflow/models. Modifying a model under /conf has zero runtime impact until the model is synchronized.
2. Workflow Instances and Lifecycle States
When a workflow is initiated (either manually via the AEM UI, programmatically via Java APIs, or automatically via a Launcher), an instance is spawned under /var/workflow/instances/serverX/<date>/<instance-id>.
A workflow instance transitions through standardized lifecycle states:
RUNNING: The workflow is actively executing automated steps or awaiting human input on participant steps.COMPLETED: All execution paths have reached the terminal end node successfully.SUSPENDED: Execution is temporarily paused by an administrator via the Workflow console.ABORTED: Execution was forcefully terminated prior to natural completion.
Workflow Step Types & Orchestration Patterns
AEM workflow models are constructed by connecting discrete step types:
+-------------------------+
| Participant Step | ==> Human inbox task
+-------------------------+
|
v
+-------------------------+
| Dialog Participant Step | ==> Human task + Granite UI Dialog
+-------------------------+
|
v
+-------------------------+
| Process Step | ==> Automated Java WorkflowProcess
+-------------------------+
|
+------------+------------+
| |
v v
+-----------------------+ +-----------------------+
| OR Split | | AND Split |
| (Executes ONE branch) | | (Executes ALL branches|
+-----------------------+ +-----------------------+
1. Participant Step
Assigns a task to a designated AEM user or user group (e.g., content-approvers). When the workflow reaches this step, it pauses execution and generates a work item in the assignee's AEM Inbox (/aem/inbox). The assignee opens the inbox item, reviews the payload (such as a page or asset), and clicks Complete to route the workflow to the next step.
2. Dialog Participant Step
Extends the standard Participant Step by presenting a Granite UI Coral 3 dialog to the user inside their AEM Inbox when they complete the task. This step is essential when business workflows require human input, such as entering an approval/rejection comment, selecting a publishing embargo date, or choosing a downstream reviewer. Values submitted through the dialog are automatically stored in the workflow's instance metadata (workItem.getWorkflowData().getMetaDataMap()) for downstream steps to inspect.
3. Process Step
Executes automated, unattended backend logic. Process steps can execute either legacy ECMA scripts or modern OSGi Java services implementing the WorkflowProcess interface.
4. OR Split vs. AND Split Branching
Understanding workflow branching mechanics is essential for exam candidates:
- OR Split: Provides conditional routing where exactly one execution branch is followed. The decision can be governed by:
- Rule Definition: JCR property evaluation.
- ECMA Script: Custom script returning a boolean or branch index.
- Participant Choice: The preceding human user selects which branch to route the work item toward.
- AND Split: Initiates parallel execution where all branches are followed simultaneously. The workflow engine forks independent execution threads for each branch. Downstream processing halts at the join step until every parallel branch reaches the join point.
Implementing Custom Java Process Steps (WorkflowProcess)
Custom Java process steps are implemented as OSGi Declarative Services implementing the com.adobe.granite.workflow.exec.WorkflowProcess interface.
Complete Implementation Example
The following code demonstrates a production-grade custom process step that validates page metadata and reads process arguments:
package com.myproject.core.workflows;
import com.adobe.granite.workflow.WorkflowException;
import com.adobe.granite.workflow.WorkflowSession;
import com.adobe.granite.workflow.exec.WorkItem;
import com.adobe.granite.workflow.exec.WorkflowData;
import com.adobe.granite.workflow.exec.WorkflowProcess;
import com.adobe.granite.workflow.metadata.MetaDataMap;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ValueMap;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.jcr.Node;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
@Component(
service = WorkflowProcess.class,
property = {
"process.label=MyProject - Validate Page Metadata Step"
}
)
public class ValidatePageMetadataProcess implements WorkflowProcess {
private static final Logger LOG = LoggerFactory.getLogger(ValidatePageMetadataProcess.class);
private static final String TYPE_JCR_PATH = "JCR_PATH";
private static final String TYPE_JCR_UUID = "JCR_UUID";
@Override
public void execute(WorkItem workItem, WorkflowSession workflowSession, MetaDataMap metaDataMap)
throws WorkflowException {
WorkflowData workflowData = workItem.getWorkflowData();
String payloadType = workflowData.getPayloadType();
String payloadPath = null;
// 1. Safely resolve payload path handling both JCR_PATH and JCR_UUID
try {
if (TYPE_JCR_PATH.equals(payloadType)) {
payloadPath = workflowData.getPayload().toString();
} else if (TYPE_JCR_UUID.equals(payloadType)) {
Session jcrSession = workflowSession.adaptTo(Session.class);
if (jcrSession != null) {
String uuid = workflowData.getPayload().toString();
Node targetNode = jcrSession.getNodeByIdentifier(uuid);
payloadPath = targetNode.getPath();
}
}
} catch (RepositoryException e) {
throw new WorkflowException("Failed to resolve payload from UUID", e);
}
if (payloadPath == null) {
LOG.warn("Cannot execute workflow step: payload path is null or unsupported type: {}", payloadType);
return;
}
// 2. Read configured Process Arguments from MetaDataMap
String processArgs = metaDataMap.get("PROCESS_ARGS", String.class);
LOG.info("Executing workflow on payload: {} with arguments: {}", payloadPath, processArgs);
// 3. Adapt WorkflowSession to Sling ResourceResolver for business logic
ResourceResolver resourceResolver = workflowSession.adaptTo(ResourceResolver.class);
if (resourceResolver != null) {
Resource payloadResource = resourceResolver.getResource(payloadPath);
if (payloadResource != null) {
Resource contentResource = payloadResource.getChild("jcr:content");
if (contentResource != null) {
ValueMap properties = contentResource.getValueMap();
String pageTitle = properties.get("jcr:title", String.class);
LOG.info("Validated page title for {}: {}", payloadPath, pageTitle);
}
}
}
}
}
Deep Dive into Method Arguments
WorkItem: Represents the specific active step instance. ThroughworkItem.getWorkflowData(), developers retrieve the payload object and instance-level metadata map.WorkflowSession: The active workflow engine session. Can be adapted via.adaptTo(Session.class)to gain JCR access or.adaptTo(ResourceResolver.class)to access the Sling resource tree.MetaDataMap: Contains configuration parameters specified on the step in the Workflow Model Editor. When a template author enters a comma-separated string in the "Process Arguments" input box of the step dialog, it is retrieved usingmetaDataMap.get("PROCESS_ARGS", String.class).
Payload Resolution: JCR_PATH vs. JCR_UUID
A critical exam trap involves assuming a workflow payload is always a JCR path string. In AEM, payloads can be submitted as:
JCR_PATH:workflowData.getPayload()returns a direct repository path string (e.g.,/content/wknd/us/en/article).JCR_UUID: In DAM asset workflows and certain programmatic triggers,workflowData.getPayload()returns a 36-character JCR Node UUID string. Passing a UUID directly toresourceResolver.getResource(uuid)returnsnull, causing an immediateNullPointerException. Developers must check the payload type and usejcrSession.getNodeByIdentifier(uuid).getPath()when resolving UUID payloads.
Workflow Launchers & Event-Driven Triggers
Workflow Launchers automatically trigger workflow models upon JCR repository events without user intervention. Launchers are configured under /conf/global/settings/workflow/launcher (and synchronized to /var/workflow/launcher).
Launcher Configuration Properties
| Property | Description | Example |
|---|---|---|
| Event Type | The JCR event that triggers evaluation | Created (node added), Modified (property altered), Removed |
| Node Type | The exact primary node type to monitor | cq:PageContent, dam:AssetContent |
| Glob Path | Regular expression path pattern restricting scope | /content/wknd/(.*)/jcr:content |
| Workflow Model | Path to the target runtime model under /var/workflow | /var/workflow/models/wknd-approval-process |
| Conditions | Property condition expression required to fire | jcr:content/cq:distribute=true |
| Exclude List | Comma-separated list of properties to ignore | jcr:lastModified,cq:lastModified,jcr:lastModifiedBy |
The Fatal Infinite Loop Anti-Pattern & Defense
The most dangerous architectural hazard when configuring Workflow Launchers is the Recursive Infinite Loop:
[ Launcher fires on Modified ] ===> [ Workflow executes Process Step ]
^ |
| v
+======== [ Process Step modifies property on payload ]
- A launcher is configured to listen for
Modifiedevents oncq:PageContentunder/content/wknd. - When an author modifies a page, the launcher triggers the workflow.
- A Java
WorkflowProcessstep executes and updates a property (e.g.,approvalStatus="in_review") on the page'sjcr:contentnode, saving the session. - Saving the session emits a new JCR
Modifiedevent oncq:PageContent. - The launcher detects the
Modifiedevent and launches a second instance of the same workflow. - The loop continues recursively, spawning thousands of concurrent workflow instances, exhausting thread pools, saturating CPU, and bringing the AEM instance down.
Architectural Defenses against Infinite Loops
- Strict Exclude Lists: Always add the property being updated by the workflow to the launcher's Exclude List (e.g., excluding
approvalStatus). The launcher engine will ignore modification events that only alter excluded properties. - Conditional Guards: Add strict conditions on the launcher (e.g.,
approvalStatus!=in_review) so the launcher immediately skips execution if the property is already set. - Listen on Specific Properties: Where possible, avoid generic
Modifiedlaunchers across broad trees; trigger workflows programmatically via Sling Event Handlers or explicit button actions.
Workflow Maintenance & Purging Best Practices
Every workflow execution creates persistent JCR nodes under /var/workflow/instances. A single workflow run creates a parent instance node along with child nodes for every work item, history entry, and metadata property. In an enterprise system executing tens of thousands of asset and page workflows weekly, /var/workflow/instances can quickly balloon to millions of JCR nodes, resulting in repository bloat, degraded TarMK performance, slow Lucene index traversals, and lengthy backup windows.
Granite Workflow Purge Scheduler
AEM provides an automated maintenance job: the Granite Workflow Purge Scheduler (com.adobe.granite.workflow.purge.Scheduler). Configured via OSGi .cfg.json in source control, it periodically purges obsolete workflow instances.
Sample OSGi configuration file com.adobe.granite.workflow.purge.Scheduler~wknd.cfg.json:
{
"scheduledpurge.name": "WKND Weekly Workflow Purge",
"scheduledpurge.workflowStatus": [
"COMPLETED",
"ABORTED"
],
"scheduledpurge.models": [
"*"
],
"scheduledpurge.daysold": 30,
"scheduledpurge.cron": "0 0 2 ? * SUN"
}
scheduledpurge.workflowStatus: Defines which statuses to purge. Best practice is to purge bothCOMPLETEDandABORTEDinstances.RUNNINGinstances should never be purged automatically.scheduledpurge.daysold: Sets the cutoff retention threshold (e.g., 30 days). Any completed workflow older than 30 days is permanently deleted from/var/workflow/instances.scheduledpurge.cron: Cron expression defining when the maintenance job runs (e.g., every Sunday at 2:00 AM off-peak).
An AEM developer is writing a custom Java WorkflowProcess step to validate newly uploaded digital assets. When testing with assets initiated from the DAM update workflow, workItem.getWorkflowData().getPayload() returns a 36-character string like '4a3b2c1d-5e6f-7a8b-9c0d-1e2f3a4b5c6d' instead of a repository path. How should the developer retrieve the target JCR Node?
A developer configures a Workflow Launcher that listens for Modified events on cq:PageContent nodes under /content/wknd. The triggered workflow executes a custom Process Step that updates an 'approvedDate' property on the page's jcr:content node. Shortly after deployment, the AEM publish instances experience 100% CPU saturation and thousands of workflow instances appear in /var/workflow/instances. What caused this issue and how should it be resolved?
How does the execution behavior of an OR Split step differ from an AND Split step in an AEM workflow model?
A system administrator notices that the JCR repository under /var/workflow/instances contains over 500,000 nodes, resulting in slow query performance and delayed backup snapshots. Which administrative solution should be implemented to permanently resolve this issue?