For AI agents: the complete documentation index is available at https://nhanbe.id.vn/llms.txt, the full documentation bundle is available at https://nhanbe.id.vn/llms-full.txt, and this page is available as Markdown at https://nhanbe.id.vn/aem/backend/workflows.md.
AEM Workflows are automated, multi-step processes that model real-world business operations inside Adobe Experience Manager. They connect people, systems, and content by defining who needs to do what, in which order, and under which conditions. Whether it is a simple two-step content approval or a complex multi-department campaign launch, workflows provide a structured, auditable way to move work forward.
Before writing any code, it helps to think of AEM Workflows as digital versions of the processes your organisation already follows on paper, in email chains, or in project management tools.
A workflow model is the blueprint - the reusable definition of steps, routes, and logic. Models are stored in the JCR under:
/var/workflow/models/ <-- runtime (compiled) models/conf/global/settings/workflow/models/ <-- editable model definitions
Each model has a unique ID (e.g. /var/workflow/models/request_for_activation) and can be versioned. When you edit a model in the Workflow Editor, AEM creates a new version and syncs it to /var/workflow/models/.
When a workflow model is started against a payload, AEM creates a workflow instance - a running copy that tracks the current step, participant assignments, and metadata.
/var/workflow/instances/ <-- active (running) instances/var/workflow/instances/server0/ <-- clustered: partitioned by server
Completed instances are moved to an archive area and can be purged for performance.
Without a proper excludeList, a workflow that modifies the payload node will re-trigger its own launcher, creating an infinite loop. Always exclude metadata properties that your workflow writes.
When a workflow reaches a Participant Step, AEM creates a work item in the assigned user's or group's Inbox (accessible at /aem/inbox). The participant can:
Complete the step (advance the workflow)
Delegate the step to another user
Step back to a previous step
Add comments for audit trail
Participants can be assigned as:
A specific user (admin, jdoe)
A group (content-authors, reviewers)
Dynamic via a ParticipantStepChooser (programmatic assignment at runtime)
Navigate to Tools > Workflow > Models to create or edit workflow models. The visual editor lets you drag and drop steps onto a canvas and connect them with transitions.
Steps to create a new model:
Click Create > Create Model
Enter a title and name (the name becomes the model ID)
Click the model to open the editor
Drag steps from the side panel onto the canvas
Connect steps by dragging from one step's output to another step's input
Configure each step by double-clicking it
Click Sync to publish the model to /var/workflow/models/
[!-success] -success
tip
Always click Sync after editing. The workflow editor works on a draft version - changes are not active until you sync.
The payload can be a page path, an asset path, or a JCR path. Here's how to access it safely:
// Get the raw payload (usually a String path)String payloadPath = workItem.getWorkflowData().getPayload().toString();// Get the payload typeString payloadType = workItem.getWorkflowData().getPayloadType();// Common values: "JCR_PATH", "JCR_UUID", "URL"// Adapt to a resourceResourceResolver resolver = workflowSession.adaptTo(ResourceResolver.class);Resource resource = resolver.getResource(payloadPath);// Adapt to a PagePageManager pageManager = resolver.adaptTo(PageManager.class);Page page = pageManager.getPage(payloadPath);// Adapt to an AssetAsset asset = resource.adaptTo(Asset.class);
An OR Split routes execution to one of several branches based on a condition. The typical pattern is:
A Process Step before the OR Split writes a boolean variable into the workflow's MetaDataMap.
Each OR Split branch has a Script (or Rule) expression that reads the variable and returns true / false.
The Process Step is a normal WorkflowProcess:
package com.myproject.core.workflow;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.WorkflowProcess;import com.adobe.granite.workflow.exec.WorkflowData;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;@Component( service = WorkflowProcess.class, property = { "process.label=My Project - Set Publish Ready Flag" })public class PublishReadyCheckStep implements WorkflowProcess { @Override public void execute(WorkItem workItem, WorkflowSession session, MetaDataMap args) throws WorkflowException { WorkflowData workflowData = workItem.getWorkflowData(); Object payload = workflowData != null ? workflowData.getPayload() : null; if (payload == null || !"JCR_PATH".equals(workflowData.getPayloadType())) { throw new WorkflowException("Expected a JCR_PATH payload"); } String payloadPath = payload.toString(); ResourceResolver resolver = session.adaptTo(ResourceResolver.class); if (resolver == null) { throw new WorkflowException("Could not obtain ResourceResolver"); } Resource jcrContent = resolver.getResource(payloadPath + "/jcr:content"); boolean publishReady = false; if (jcrContent != null) { ValueMap props = jcrContent.getValueMap(); publishReady = props.containsKey("jcr:title") && props.containsKey("jcr:description") && props.containsKey("featuredImage"); } // Store the result so the OR Split branches can read it workItem.getWorkflow().getWorkflowData().getMetaDataMap() .put("publishReady", publishReady); }}
In the OR Split, the branches must be complementary, not identical. Give the "ready" branch a routing expression type of Script, with the language set to ECMA Script, and paste the function into the inline script field:
function check() { return workflowData.getMetaDataMap().get("publishReady", false); }
Mark the other branch as the Default route (taken when no other branch's condition matches), so exactly one branch fires. If you need an explicit condition on both sides instead of a default, negate the check on the second branch: return !workflowData.getMetaDataMap().get("publishReady", false);
Script Path is a separate option that points at a repository node holding the same code as an external .ecma file, rather than accepting inline text.
/** * Disables all workflow launchers. Useful during bulk imports or migrations * to prevent unwanted workflow triggers. */public void disableAllLaunchers(ResourceResolver resolver) throws LoginException { WorkflowLauncher launcherService = resolver.adaptTo(WorkflowLauncher.class); if (launcherService == null) { LOG.error("Could not obtain WorkflowLauncher service"); return; } Iterator<ConfigEntry> entries = launcherService.getConfigEntries(); while (entries.hasNext()) { ConfigEntry entry = entries.next(); if (entry.isEnabled()) { entry.setEnabled(false); LOG.info("Disabled launcher: {}", entry.getId()); } }}/** * Re-enables all workflow launchers. */public void enableAllLaunchers(ResourceResolver resolver) { WorkflowLauncher launcherService = resolver.adaptTo(WorkflowLauncher.class); if (launcherService == null) return; Iterator<ConfigEntry> entries = launcherService.getConfigEntries(); while (entries.hasNext()) { ConfigEntry entry = entries.next(); if (!entry.isEnabled()) { entry.setEnabled(true); LOG.info("Enabled launcher: {}", entry.getId()); } }}
[!-success] -success
tip
A common pattern during bulk content migrations: disable launchers before the import, run the import, then re-enable launchers. This avoids thousands of workflow instances being created for every imported node.
Instead of programmatic toggling, you can permanently disable a built-in launcher by overlaying its configuration. This is deployed as part of your code package and survives instance restarts.
Create an overlay at /conf/global/settings/workflow/launcher/config/<launcher-name>/.content.xml with enabled="{Boolean}false":
<?xml version="1.0" encoding="UTF-8"?><jcr:root xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:cq="http://www.day.com/jcr/cq/1.0" jcr:primaryType="cq:WorkflowLauncher" comment="Disabled - we use Adobe Campaign for newsletters" enabled="{Boolean}false" eventType="{Long}1" excludeList="" glob="/var/mailimport" nodetype="nt:unstructured" workflow="/var/workflow/models/newsletter_bounce_check"/>
Make sure this path is included in your package's META-INF/vault/filter.xml:
The DAM Update Asset workflow is AEM's built-in pipeline for processing uploaded assets. It runs automatically when a new asset is added or an existing asset is replaced.
Default steps include:
Metadata extraction - reads EXIF, IPTC, XMP data from the file
Rendition generation - creates thumbnails and web-optimized renditions
Sub-asset extraction - extracts pages from PDFs, slides from presentations
XMP writeback - writes AEM metadata changes back into the binary
Full-text extraction - indexes text content for search
AEMaaCS On AEM as a Cloud Service, the DAM Update Asset workflow has been replaced by
Asset microservices. Custom Java workflow steps for rendition generation do not run on AEMaaCS. Instead:
Rendition generation is handled by Adobe's cloud-native microservices
Custom processing uses Processing Profiles (configured in the DAM admin UI)
For advanced custom processing, use Asset Compute workers (Adobe I/O Runtime)
Post-processing workflows can still run custom Java steps for metadata operations, but not binary manipulation
If you are building for AEMaaCS, plan your DAM workflow customisations around Processing Profiles and Asset Compute rather than custom WorkflowProcess steps.
#Use transient workflows for high-volume processing
Transient workflows don't persist intermediate step data to the JCR. This dramatically improves performance for workflows that process many items (like DAM Update Asset).
Enable transient mode by checking Transient in the workflow model editor's page properties.
If your workflow modifies the payload (e.g. sets a property), the modification can re-trigger the launcher, creating an infinite loop. Prevent this by:
excludeList - list properties that should not re-trigger the launcher:
Workflow process steps run on the AEM server thread pool. A step that takes minutes (e.g. calling a slow external API) blocks the thread and can starve the workflow engine.
For long-running operations:
Use a Sling Job (via JobManager) and have the workflow step start the job, then use an External Process Step that waits for a callback
Set timeouts on HTTP calls
Consider asynchronous patterns with a polling step