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.
Last updated: September 2026

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 using WorkflowProcess, safely resolving JCR_PATH vs. JCR_UUID payload types, passing metadata arguments via MetaDataMap, preventing infinite launcher loops on Modified events, 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

  1. WorkItem: Represents the specific active step instance. Through workItem.getWorkflowData(), developers retrieve the payload object and instance-level metadata map.
  2. 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.
  3. 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 using metaDataMap.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 to resourceResolver.getResource(uuid) returns null, causing an immediate NullPointerException. Developers must check the payload type and use jcrSession.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

PropertyDescriptionExample
Event TypeThe JCR event that triggers evaluationCreated (node added), Modified (property altered), Removed
Node TypeThe exact primary node type to monitorcq:PageContent, dam:AssetContent
Glob PathRegular expression path pattern restricting scope/content/wknd/(.*)/jcr:content
Workflow ModelPath to the target runtime model under /var/workflow/var/workflow/models/wknd-approval-process
ConditionsProperty condition expression required to firejcr:content/cq:distribute=true
Exclude ListComma-separated list of properties to ignorejcr: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 ]
  1. A launcher is configured to listen for Modified events on cq:PageContent under /content/wknd.
  2. When an author modifies a page, the launcher triggers the workflow.
  3. A Java WorkflowProcess step executes and updates a property (e.g., approvalStatus="in_review") on the page's jcr:content node, saving the session.
  4. Saving the session emits a new JCR Modified event on cq:PageContent.
  5. The launcher detects the Modified event and launches a second instance of the same workflow.
  6. 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 Modified launchers 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 both COMPLETED and ABORTED instances. RUNNING instances 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).
Test Your Knowledge

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
B
C
D
Test Your Knowledge

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?

A
B
C
D
Test Your Knowledge

How does the execution behavior of an OR Split step differ from an AND Split step in an AEM workflow model?

A
B
C
D
Test Your Knowledge

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?

A
B
C
D