AEM Workflows: The Complete Guide
A practical guide to AEM Workflows — workflow models, launchers, the step types (process, participant, dynamic participant, and more), and how to write a custom workflow process in Java, with payload access, routing, and transient workflows. Includes code, a cheat sheet, best practices, and do's & don'ts.
Workflows are how AEM automates the processes around content: review-and-approve before publishing, the asset processing that runs on every upload, translation, scheduled activation, and any custom business logic you need to chain together. Anything that should happen "when X, then do Y, then route to Z for approval" is a workflow.
This guide explains the whole model — how a workflow model is designed, how launchers start one automatically, the step types you assemble it from (with a focus on process and participant steps), and how to write your own custom workflow process in Java. There's runnable code throughout, and a cheat sheet, best practices, and do's & don'ts at the end.
It expands on the workflow section of the Backend Development guide, relates to the Assets guide (the DAM Update Asset workflow), and contrasts with Sling Jobs from the Sling guide.
The core concepts
A few terms recur throughout, so it's worth pinning them down first:
- A workflow model is the design — a sequence of steps connected by transitions. It's the template.
- A workflow instance is a running execution of a model against a specific piece of content.
- The payload is the content the workflow acts on — usually a page or asset path (a
JCR_PATH). - A work item is the current active step of an instance; for a human step it lands in someone's Inbox.
- A launcher is a rule that starts a workflow automatically when content changes.
In short: you design a model once, a launcher (or a user, or code) starts an instance of it against a payload, and the instance moves from step to step, occasionally pausing on a work item for a human.
Workflow Models
A workflow model is built visually in the Workflow editor (/libs/cq/workflow/content/console.html), where you drag steps onto a flow and connect them with transitions. On modern AEM, models are stored as editable definitions under /conf/global/settings/workflow/models, with a synced runtime copy under /var/workflow/models that instances actually execute.
Models can express real control flow, not just a straight line:
- Transitions connect steps in sequence.
- OR splits branch down one of several paths based on a condition (e.g. route by region).
- AND splits run multiple branches in parallel and rejoin.
- Containers embed a sub-workflow, so you can compose models from reusable pieces.
- Goto steps jump to another step, enabling loops and conditional skips.
The two practical things to know: edit the model in /conf (and deploy it as code), and remember that there's a model vs. runtime model distinction — if a model change doesn't seem to take effect, the runtime copy may need to sync.
Launchers
Most workflows don't get started by a person clicking a button — they're triggered automatically by a launcher. A launcher watches the repository and, when content matching its conditions changes, kicks off a workflow model. This is exactly how the DAM Update Asset workflow runs on every asset upload.
You configure launchers in the Workflow console's Launchers tab (stored under /conf), and each one binds a set of conditions to a model:
| Launcher field | Purpose |
|---|---|
| Event Type | Created, Modified, or Removed |
| Node Type | The type to watch (e.g. dam:Asset, cq:PageContent) |
| Path | A glob restricting where it applies (e.g. /content/dam/mysite(/.*)?) |
| Condition | A property test (e.g. jcr:content/jcr:title != null) |
| Workflow | The model to start |
| Run Modes | Restrict to author/publish |
| Exclude List | Users/paths to ignore (avoids loops) |
Important: Launchers are a classic source of infinite loops — if a workflow modifies the very content that launched it, it can re-trigger itself. Scope the launcher tightly (path, node type, condition) and use the exclude list / a "workflow service user" exclusion so a workflow's own writes don't relaunch it.
The step types
A model is assembled from steps, and AEM provides several built-in types. Knowing what each does tells you which to drag onto the canvas:
| Step | What it does |
|---|---|
| Process Step | Runs Java (or ECMAScript) automatically — no human |
| Participant Step | Assigns a work item to a user/group; pauses for their action |
| Dynamic Participant Step | Like Participant, but the assignee is chosen by code |
| Dialog Participant Step | A participant step that also collects data via a dialog |
| Container Step | Runs another workflow model as a sub-process |
| OR / AND Split | Conditional branch / parallel branches |
| Goto Step | Jump to another step (loops, conditional skips) |
| Set Variable | Set workflow metadata/variables for later steps |
The two you'll work with most — and write code for — are process and participant steps.
Process Steps
A process step runs automatically with no human involvement — it's where your Java logic lives. Validate content, transform an asset, call an external service, replicate a page: all process steps. You implement the WorkflowProcess interface and register it as an OSGi service with a process.label, which is the name authors pick from in the step's configuration.
@Component(
service = WorkflowProcess.class,
property = { "process.label=My Site - Validate Page" })
public class ValidatePageStep implements WorkflowProcess {
@Override
public void execute(WorkItem workItem, WorkflowSession wfSession, MetaDataMap args)
throws WorkflowException {
String path = workItem.getWorkflowData().getPayload().toString();
// ... run validation; throw WorkflowException to fail the step ...
}
}
Because process steps run inside the workflow engine, keep them fast and reliable — the full "custom process" treatment, including payload access, arguments, and routing, is the section below.
Participant Steps
A participant step is where a human enters the loop. When an instance reaches one, it pauses and creates a work item assigned to a user or group, which appears in their Inbox; the workflow only continues once that person acts. This is the backbone of review-and-approve flows: an author submits a page, it pauses at "Legal Review," and only an approver's decision advances (or rejects) it.
When you don't know the assignee at design time, use a Dynamic Participant Step, where a ParticipantStepChooser picks the assignee in code — for example, routing to a region-specific approver group based on the page's path:
@Component(
service = ParticipantStepChooser.class,
property = { "chooser.label=My Site - Region Approver" })
public class RegionApproverChooser implements ParticipantStepChooser {
@Override
public String getParticipant(WorkItem workItem, WorkflowSession wfSession, MetaDataMap args)
throws WorkflowException {
String path = workItem.getWorkflowData().getPayload().toString();
return path.contains("/eu/") ? "eu-approvers" : "global-approvers"; // group id
}
}
The returned string is the principal (user or group id) the work item is assigned to. A Dialog Participant Step goes further, presenting the participant a form so they can supply data the workflow then acts on.
Custom Workflow Process (in depth)
The custom process step is where most real workflow development happens, so here's the full picture. The execute method receives three objects, and together they give you everything you need:
WorkItem— the current step, and your handle to the payload and workflow data.WorkflowSession— lets you advance, route, restart, or terminate the workflow.MetaDataMap— delivers the step's configured arguments and lets you read/write workflow metadata shared across steps.
A complete step that reads its payload and arguments, does work, and routes the workflow looks like this:
@Component(
service = WorkflowProcess.class,
property = { "process.label=My Site - Approve & Publish" })
public class ApproveAndPublishStep implements WorkflowProcess {
@Reference
private Replicator replicator;
@Override
public void execute(WorkItem workItem, WorkflowSession wfSession, MetaDataMap args)
throws WorkflowException {
WorkflowData data = workItem.getWorkflowData();
if (!"JCR_PATH".equals(data.getPayloadType())) return;
String path = data.getPayload().toString();
// Step arguments configured on the model (the PROCESS_ARGS field)
String mode = args.get("PROCESS_ARGS", String.class);
try {
Session session = wfSession.adaptTo(Session.class);
replicator.replicate(session, ReplicationActionType.ACTIVATE, path);
} catch (ReplicationException e) {
throw new WorkflowException("Publish failed for " + path, e);
}
// Optional explicit routing through an OR split:
// List<Route> routes = wfSession.getRoutes(workItem);
// wfSession.complete(workItem, routes.get(0));
}
}
A few things worth calling out:
- Read the payload via
workItem.getWorkflowData().getPayload(), and checkgetPayloadType()isJCR_PATHbefore trusting it. - Arguments come through
args.get("PROCESS_ARGS", String.class)— the value authors type into the step's "Arguments" field. - Routing: for a normal linear step you don't route manually; for an OR split, call
wfSession.getRoutes(workItem)andwfSession.complete(workItem, route)to choose a branch. - Failure: throw a
WorkflowExceptionto fail the step (it can then be retried or surfaced to an admin). - Heavy or unreliable work (slow external calls) should be pushed to a Sling Job so the step completes quickly and the engine isn't blocked.
For performance-sensitive automation (like bulk asset processing), consider a transient workflow — a model flagged transient keeps no instance history in the repository, which makes it much faster, at the cost of no audit trail and no participant steps.
Tip: Workflows excel at human-in-the-loop and content-lifecycle processes. For purely automated, high-volume, fire-and-forget work, a Sling Job is often the better tool — it's distributed, retried, and doesn't carry workflow overhead. Use a workflow when you need steps, approvals, and history; a job when you just need reliable background processing.
Cheat sheet
| Need | Use | Registered as |
|---|---|---|
| Design a process | Workflow model | /conf/.../workflow/models |
| Auto-start on content change | Launcher | Workflow console / /conf |
| Run Java automatically | Process step | WorkflowProcess + process.label |
| Human approval/action | Participant step | assign to user/group |
| Choose assignee in code | Dynamic participant | ParticipantStepChooser + chooser.label |
| Branch the flow | OR / AND split | model step |
| Read the content | workItem.getWorkflowData().getPayload() | — |
| Read step arguments | args.get("PROCESS_ARGS", String.class) | — |
| Route an OR split | wfSession.getRoutes() + complete() | — |
| High-volume automation | Sling Job (instead) | JobConsumer |
Best practices
- ✅ Store models in
/confand deploy them as code. - ✅ Scope launchers tightly (path, node type, condition) and exclude the workflow service user to prevent loops.
- ✅ Keep process steps fast; offload heavy/unreliable work to Sling Jobs.
- ✅ Use dynamic participant steps to route approvals programmatically.
- ✅ Use transient workflows for high-volume, no-audit automation.
- ✅ Throw
WorkflowExceptionon failure so steps fail visibly and can retry.
Do's and Don'ts
Do
- ✅ Check
getPayloadType()isJCR_PATHbefore using the payload. - ✅ Replicate/act with a properly-permissioned service user, not admin.
- ✅ Choose Sling Jobs over workflows for pure background processing.
Don't
- ❌ Don't let a workflow modify the content that launched it without an exclusion — infinite loop.
- ❌ Don't make slow external calls inline in a process step — enqueue a job.
- ❌ Don't use participant steps in a transient workflow (unsupported).
- ❌ Don't hard-code approver IDs when a dynamic participant chooser fits.
- ❌ Don't forget to deploy/sync the model — runtime executes the synced copy.
Wrapping up
AEM Workflows turn content operations into repeatable, automated processes. Design the flow as a model, start it automatically with a launcher, and assemble it from process steps (your Java) and participant steps (human decisions) — with dynamic participants, splits, and containers for anything more complex. When you write a custom process, the WorkItem, WorkflowSession, and MetaDataMap give you the payload, routing, and arguments you need; keep steps fast and push heavy work to jobs. Master that and you can automate everything from a simple approval to a full content-lifecycle pipeline.
Continue with the Backend Development guide for event handlers and listeners that complement workflows, the Sling guide for when a job is the better choice, and the Assets guide for the asset-processing workflow in action.
Subscribe to the Newsletter
Get the latest articles, tutorials, and tech insights delivered straight to your inbox. No spam, unsubscribe anytime.