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/advanced/workflow.md.

Workflows

09 – Workflows

Target: 3–4 years experienced AEM Developer Syllabus point covered (19, first half): "Scheduler/workflow examples. Say you haven't done it but you know about it." Project domain: a global energy technology company's marketing site.


Before we start — about that note in your syllabus

Your syllabus point 19 says something the others don't:

"scheduler/workflow ke examples. bol dena kiya nai par pata hai " — "say you haven't done it, but you know about it."

That is a note the candidate wrote to themselves about how to answer, not about what to learn. And it is genuinely good instinct, so this file is built to support it properly.

Why it's the right instinct. Workflows are a topic where plenty of AEM developers have real gaps. Many projects use the out-of-the-box workflows and never write a custom process step. Claiming deep hands-on experience you don't have is dangerous here, because the follow-ups get specific fast — "where is the model stored?", "what does your process step's execute signature look like?", "how did you get a resource resolver inside it?" — and vague answers to those are worse than a clean admission.

But "I haven't done it" on its own scores zero. The version that scores well has three parts:

  1. Say what you have seen. Almost everyone has used workflows even if they haven't built one — approving a page, watching DAM Update Asset run, seeing items in the Inbox.
  2. Demonstrate you understand the model. Structure, storage, step types, how a custom step is written.
  3. Say how you'd approach it. Concretely enough that it's obvious you could.

Done that way, an honest answer often lands better than an inflated one, because interviewers can tell the difference and they are actively watching for it. Section 6.1 is that answer written out.

One more thing worth knowing before you decide how to pitch it. On AEM as a Cloud Service, asset processing has moved off workflows to Asset Compute microservices, and long-running workflows are actively discouraged because pods get recycled. So workflows matter less on modern projects than they did on 6.5 — and saying that shows current awareness rather than a gap. Section 2.9.


1. Introduction

1.1 What is a workflow?

A workflow is a defined sequence of steps that automates a business process in AEM — including steps that require a human.

That last clause is what makes workflows distinct from everything else in AEM. A scheduler runs code on a timer. A Sling Job processes something asynchronously. Only a workflow can stop and wait for a person.

A concrete example from our domain. A product page for a high-voltage transformer cannot simply be published by whoever edited it. Technical claims have to be checked by an engineer, and regulatory statements by compliance. So:

Author finishes the page
        ↓
Technical review        ← a human step. The workflow WAITS.
        ↓
Compliance review       ← another human step
        ↓
Publish                 ← automatic once both approve
        ↓
Notify the author

That process has waiting, branching, accountability and an audit trail. That is what workflows are for.

1.2 When do you use a workflow — and when don't you?

This is the question interviewers actually care about, because reaching for a workflow when you don't need one is a common design mistake.

Use a workflow when:

  • A human must approve or act.
  • You need an audit trail — who approved what, and when.
  • The process is long-running and may pause for hours or days.
  • Business users need to see and manage it in a UI.

Don't use a workflow when:

  • It's just time-based — that's a Scheduler.
  • It's bulk async processing with no human involved — that's a Sling Job.
  • It needs to happen during a request — that's a servlet or a service.

The comparison in one line, which is worth memorising:

"Scheduler is time. Job is throughput. Workflow is people and paper trail."

1.3 What you've almost certainly seen, even if you haven't built one

Worth cataloguing, because this is the honest foundation of a good answer:

DAM Update Asset — runs whenever an asset is uploaded, generating renditions and extracting metadata. On AEM 6.5 this is a workflow. If you've ever uploaded an image and watched thumbnails appear, you've watched a workflow run.

Request for Activation — when an author lacks publish rights, activating creates a workflow that sends the request to an approver's Inbox.

Translation workflows — created when you start a translation project.

Scheduled activation — publishing a page with an on-time creates a workflow that waits.

Saying this in an interview is a genuinely strong move, because it is true, it's specific, and it converts "I haven't done it" into "here's what I've actually observed."


2. Core Concepts

2.1 The five pieces

Every workflow question comes back to these. Learn them as a set.

graph TD A["LAUNCHER<br/>watches for a content event"] -->|"triggers"| B["MODEL<br/>the definition — the steps"] B -->|"creates"| C["INSTANCE<br/>one running execution"] C -->|"carries"| D["PAYLOAD<br/>the content being acted on"] C -->|"executes"| E["STEPS<br/>process · participant · split"] E -->|"human steps appear in"| F["INBOX"]

PieceWhat it is
ModelThe definition — the steps and their order. Like a class.
InstanceOne running execution of a model. Like an object.
LauncherA rule that starts a workflow when something happens in the repository
PayloadThe content the workflow is acting on — usually a page or asset path
StepOne unit of work — automated, or assigned to a person

The model/instance distinction is the one to be crisp about. A model is the design; an instance is one run of it. Ten authors submitting ten pages produces one model and ten instances. That is also why /var/workflow/instances grows and needs purging — every run leaves a record.

2.2 Where everything is stored

Interviewers ask this, and the answer changed across versions — which is worth knowing.

WhatWhere
Model (design)/conf/global/settings/workflow/models/<model-name>
Model (runtime)/var/workflow/models/...
Launchers/conf/global/settings/workflow/launcher/config/
Instances/var/workflow/instances/
Inbox items/var/workflow/instances/.../workItems

Two things to notice:

Models moved out of /etc. In AEM 6.2 and earlier they lived at /etc/workflow/models. The repository restructuring in 6.4/6.5 moved them to /conf — the same shift that moved templates and clientlibs, from file 01. Mentioning that connection shows you understand the pattern rather than having memorised a path.

There are two copies of every model — a design copy under /conf and a runtime copy under /var. That surprises people, and it explains a real bug:

If you edit a workflow model and your change doesn't take effect, you probably didn't sync it. The editor has a Sync button that regenerates the runtime copy from the design copy. Until you do, running instances use the old definition.

That is a good detail to have, because it's the kind of thing only someone who has actually opened the workflow editor knows.

2.3 The step types

There are more than people expect. Knowing the range — even without having built each — demonstrates real familiarity.

Step typeWhat it does
Process StepRuns Java code. The one developers write.
Participant StepAssigns to a user or group; appears in their Inbox and waits
Dynamic Participant StepSame, but the assignee is chosen at runtime by code
Dialog Participant StepA participant step where the person fills in a dialog
Or SplitConditional branching — one path taken
And SplitParallel branches — all paths taken
Container StepRuns another workflow model inside this one
Goto StepJumps to another step — conditional loops
External Process StepFor long-running work handled outside AEM

The two that matter most:

Process Step is where developers live. It runs a Java class you wrote.

Participant Step is what makes a workflow a workflow. It assigns the item to a person or group, puts it in their Inbox, and the instance pauses indefinitely until they act.

A distinction worth having ready: "Or Split takes one branch based on a condition; And Split runs branches in parallel and rejoins." That's a common quick question.

2.4 Writing a custom process step

This is the code developers actually write, and the piece to be able to sketch even if you haven't shipped one.

@Component(
        service = WorkflowProcess.class,
        property = {
                "process.label=Energy - Notify Compliance Team"
        }
)
public class NotifyComplianceProcess implements WorkflowProcess {

    @Override
    public void execute(WorkItem workItem,
                        WorkflowSession workflowSession,
                        MetaDataMap metaDataMap) throws WorkflowException {
        // your logic
    }
}

Four things to be able to explain:

service = WorkflowProcess.class — this is an OSGi service, so everything from file 06 applies. It's registered under the WorkflowProcess interface, which is how the workflow engine finds it.

process.label — this is what appears in the dropdown when someone configures a Process Step in the workflow editor. Without it, your step is unlabelled and effectively unfindable. This is the property people forget.

The execute signature takes three things:

ParameterWhat it gives you
WorkItemThe current step, and the payload
WorkflowSessionThe workflow API — and a way to get a ResourceResolver
MetaDataMapArguments configured on the step in the editor

Getting the payload — the content the workflow is acting on:

String payloadPath = workItem.getWorkflowData().getPayload().toString();

Getting a ResourceResolver — and this is a genuinely good detail:

ResourceResolver resolver = workflowSession.adaptTo(ResourceResolver.class);

That resolver is the workflow session's, so do not close it — the same rule as the request's resolver in file 07. If you need different permissions, open your own service-user resolver in try-with-resources.

Reading step arguments:

String args = metaDataMap.get("PROCESS_ARGS", String.class);

PROCESS_ARGS is the free-text argument field on the Process Step's configuration dialog — how one Java class is reused across several steps with different settings.

2.5 Workflow Launchers

A launcher is a rule: "when this happens in the repository, start this workflow."

What you configure:

SettingPurpose
Event TypeCreated, Modified, Removed
Node Typee.g. cq:PageContent, dam:Asset
PathA regex — which part of the repository
ConditionAn optional expression for finer control
WorkflowWhich model to run
Run Modesauthor and/or publish
ActivateEnabled or not

Two things that cause real production problems, and both make good scenario answers:

Too broad a path. A launcher on /content with node type cq:PageContent fires on every page edit across the entire site. On a large site that's thousands of workflow instances an hour, and /var/workflow/instances fills the disk. Scope the path as narrowly as possible.

No run mode restriction. A launcher with no run mode set runs on publish too. An approval workflow firing on the publish tier is both pointless and a performance problem — publish should be serving pages, not running business processes. Almost every launcher should be restricted to author.

2.6 Transient workflows — the performance option

A detail that separates people who have operated AEM from people who have only read about it.

The problem. Every workflow instance creates nodes under /var/workflow/instances — the history, the work items, the metadata. For an approval workflow running a few times a day, that's fine and you want the record. For a workflow that runs on every asset upload, when you're ingesting fifty thousand product images, it's a serious repository problem.

The solution. A model can be marked transient. A transient workflow doesn't persist instance nodes at all. It runs, does its work, and leaves no trace.

NormalTransient
Instance nodes createdYesNo
Repository growthYesNone
SpeedSlowerFaster
History / audit trailYesNo
Visible in the Workflow consoleYesNo
Can have human stepsYesNo
Use forApprovals, anything auditableHigh-volume automated processing

DAM Update Asset is transient by default in newer AEM versions, for exactly this reason.

The trade-off to state: you give up the audit trail and the ability to see or restart the instance. So transient is right for automated, high-volume, low-stakes processing — and wrong for anything anyone might need to prove happened.

2.7 The out-of-the-box workflows

Being able to name several shows familiarity even without custom development:

WorkflowWhat it does
DAM Update AssetRenditions, metadata extraction, smart tags on upload
Request for ActivationApproval before publishing, when the author lacks rights
Request for DeletionApproval before deleting
Scheduled Activation / DeactivationWaits until a set time, then publishes or unpublishes
Move / Delete DAM AssetHandles references when assets move
Translation workflowsCreated by translation projects
Publish Example / Approve for Adobe CampaignSample models

2.8 Workflow versus Scheduler versus Sling Job

This is the comparison your syllabus is really asking about, since point 19 groups schedulers and workflows together. Full detail on the other two is file 10.

WorkflowSchedulerSling Job
Triggered byContent event, launcher, or manuallyTime (cron)Code, programmatically
Human stepsYesNoNo
PersistedYes (/var)NoYes
Survives a restartYesNoYes
Guaranteed to runYesNoAt least once
Cluster-awareYesNeeds careYes
Visible to business usersYes — Inbox, consoleNoJob queue console only
Audit trailYesNoLimited
Use forApprovals, business processNightly report, cache warm-upBulk async processing

The three-example answer interviewers like:

  • "Send a report every night at 2am" → Scheduler. Time-based, no human, no record needed.
  • "Process 100,000 assets" → Sling Jobs. Needs guaranteed execution and cluster awareness, but no human.
  • "Legal must approve before publish" → Workflow. A human step, an audit trail, and business users need to see it.

2.9 Workflows on AEM as a Cloud Service — what changed

This matters, and raising it unprompted turns a knowledge gap into current awareness.

Asset processing moved off workflows entirely. On 6.5, DAM Update Asset ran on the AEM instance — CPU-heavy image and video processing competing with authors for resources. On Cloud Service that moved to Asset Compute microservices, configured through Processing Profiles rather than a workflow model.

Long-running workflows are discouraged. Publish and author pods are recycled — during deployment, during scaling, or if a pod becomes unhealthy. A workflow paused mid-execution has to survive that, which is fine for a persisted approval workflow waiting on a person, but bad for a step doing hours of continuous processing.

Maintenance is automated. Adobe handles workflow purging, which on 6.5 was a task you had to configure and monitor.

What still applies: custom approval and business-process workflows work exactly as before. It's specifically the asset processing use case that moved.

Why this is worth saying:

"It's worth noting that workflows matter less on Cloud Service than they did on 6.5. Asset processing — which was the highest-volume workflow use case — moved off to Asset Compute microservices and Processing Profiles. And long-running workflows are discouraged because pods get recycled. Custom approval workflows are still workflows, but the heavy automated processing that used to be a workflow problem is now a different mechanism."

That reframes "I haven't built many workflows" as "our platform doesn't need as many," which is both honest and current.


3. Internal Working

3.1 The lifecycle of a workflow instance

sequenceDiagram participant A as Author participant L as Launcher participant E as Workflow Engine participant I as Instance (/var) participant P as Process Step participant H as Human (Inbox) A->>L: edits and saves a page L->>L: does the event match?<br/>path · node type · run mode L->>E: start the model E->>I: create the instance node E->>P: execute the first Process Step P->>P: run Java, read the payload P-->>E: done, advance E->>H: Participant Step → into the Inbox Note over E,H: The instance now WAITS.<br/>Possibly for days. H-->>E: approved E->>P: next step — activate the page E->>I: mark complete

Three things worth drawing out:

The launcher decides whether to start anything. Path, node type and run mode are all evaluated first — which is why a badly scoped launcher creates thousands of unwanted instances.

A Participant Step pauses indefinitely. The instance sits in /var consuming a little space until a person acts. If nobody ever does, it sits there forever — which is how workflows "pile up."

Every instance leaves a record unless the model is transient. That is the audit trail, and it is also the disk-space problem.

3.2 Why /var/workflow/instances fills the disk

A classic production scenario, and the mechanism is worth understanding.

graph TD A["Every workflow run creates<br/>instance nodes in /var"] --> B{"Is the model transient?"} B -->|Yes| C["Nothing persisted ✓"] B -->|No| D["Nodes persist AFTER completion<br/>— that's the audit trail"] D --> E{"Is the Workflow Purge<br/>maintenance task running?"} E -->|Yes| F["Old instances removed ✓"] E -->|No| G["Grows without bound<br/>→ DISK FULL"]

The key insight: completed instances are not deleted. They stay, deliberately, because they are the audit record. Something has to remove them, and that something is the Workflow Purge maintenance task.

Three things make it worse:

  • A launcher scoped too broadly, creating far more instances than intended.
  • Stalled instances waiting on a person who never acts — those can't be purged as completed.
  • Purge configured to keep too long a history.

That combination — too-broad launcher plus no purge — is behind most "AEM disk full" incidents on 6.5, and it is a good scenario answer because it links a design mistake to an operational failure.

3.3 Workflow failures

When a Process Step throws, the instance goes into a Failure state rather than silently dying. You can see failures in the workflow console and retry or terminate them.

Which is why exception handling in a process step matters. Throwing WorkflowException puts the instance in Failures where someone can act on it. Swallowing the exception silently marks the step complete and the workflow moves on as though it succeeded — which is worse, because the process appears to have worked.

"In a process step I'd throw WorkflowException on a genuine failure rather than swallowing it, so the instance lands in the Failures queue where someone can see and retry it. Silently catching means the workflow completes as though it succeeded, and for something like a compliance approval that's a much worse outcome than a visible failure."


8. Coding Examples

8.1 A custom process step, annotated

package com.energy.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.WorkflowProcess;
import com.adobe.granite.workflow.metadata.MetaDataMap;
import com.day.cq.wcm.api.Page;
import org.apache.sling.api.resource.ModifiableValueMap;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jcr.RepositoryException;
import java.util.Calendar;

/**
 * Marks a product page as technically reviewed.
 *
 * A workflow process step is an OSGi SERVICE -- everything from file 06
 * applies, including that it can sit unsatisfied and never appear.
 */
@Component(
        service = WorkflowProcess.class,
        property = {
                // THIS IS WHAT APPEARS IN THE DROPDOWN when someone
                // configures a Process Step in the model editor.
                // Without it, the step is effectively unfindable.
                "process.label=Energy - Mark Technically Reviewed"
        }
)
public class MarkReviewedProcess implements WorkflowProcess {

    private static final Logger LOG = LoggerFactory.getLogger(MarkReviewedProcess.class);

    @Reference
    private com.energy.core.services.NotificationService notificationService;

    /**
     * @param workItem         the current step -- carries the PAYLOAD
     * @param workflowSession  the workflow API, and a source of a ResourceResolver
     * @param metaDataMap      arguments configured on this step in the editor
     */
    @Override
    public void execute(WorkItem workItem,
                        WorkflowSession workflowSession,
                        MetaDataMap metaDataMap) throws WorkflowException {

        // ---- 1. The payload: the content this workflow is acting on ----
        String payloadPath = workItem.getWorkflowData().getPayload().toString();

        // Payload can be JCR_PATH or JCR_UUID -- check rather than assume
        String payloadType = workItem.getWorkflowData().getPayloadType();
        if (!"JCR_PATH".equals(payloadType)) {
            throw new WorkflowException("Expected a JCR_PATH payload, got " + payloadType);
        }

        // ---- 2. Step arguments from the editor's dialog ----
        // PROCESS_ARGS is the free-text argument field. It is how ONE
        // Java class serves several steps with different settings.
        String args = metaDataMap.get("PROCESS_ARGS", String.class);
        String reviewType = (args != null && !args.isEmpty()) ? args.trim() : "technical";

        // ---- 3. A ResourceResolver from the workflow session ----
        // NOTE: do NOT close this. The workflow session owns it -- same
        // rule as the request's resolver in file 07. If I needed
        // different permissions I would open my own service-user
        // resolver in try-with-resources instead.
        ResourceResolver resolver = workflowSession.adaptTo(ResourceResolver.class);
        if (resolver == null) {
            throw new WorkflowException("Could not obtain a ResourceResolver");
        }

        Resource resource = resolver.getResource(payloadPath);
        if (resource == null) {
            // The page may have been deleted while the workflow waited
            // on a human step -- a very real case for long-paused workflows.
            throw new WorkflowException("Payload no longer exists: " + payloadPath);
        }

        try {
            // ---- 4. Do the work ----
            Resource contentResource = resource.getChild("jcr:content");
            if (contentResource == null) {
                throw new WorkflowException("No jcr:content at " + payloadPath);
            }

            ModifiableValueMap properties = contentResource.adaptTo(ModifiableValueMap.class);
            if (properties == null) {
                throw new WorkflowException("Content is not modifiable: " + payloadPath);
            }

            properties.put("reviewStatus", reviewType + "-approved");
            properties.put("reviewedBy", workItem.getWorkflow().getInitiator());
            properties.put("reviewedOn", Calendar.getInstance());

            resolver.commit();

            LOG.info("Marked {} as {} reviewed", payloadPath, reviewType);

            // ---- 5. Workflow metadata: persists ACROSS steps ----
            // Work-item metadata is per step; workflow metadata is shared
            // by every step in the instance.
            workItem.getWorkflow().getWorkflowData().getMetaDataMap()
                    .put("lastReviewType", reviewType);

        } catch (Exception e) {
            // THROW, don't swallow.
            //
            // Throwing puts the instance in the FAILURES queue where a
            // human can see and retry it. Catching silently marks the
            // step complete and the workflow proceeds as though it
            // succeeded -- for a compliance approval, far worse than a
            // visible failure.
            LOG.error("Failed to mark {} as reviewed", payloadPath, e);
            throw new WorkflowException("Could not mark page as reviewed", e);
        }
    }
}

The six things to be able to point at:

process.label is what makes the step selectable in the editor. Forgetting it is the classic first mistake.

The payload type is checked, not assumed.

The resolver comes from the workflow session and is not closed — you didn't open it.

The payload may no longer exist, because a workflow can pause for days on a human step and content changes meanwhile. That check is the kind of thing only someone thinking about long-paused instances includes.

Work-item metadata versus workflow metadata — per step versus shared across the instance.

Exceptions are thrown, not swallowed, with the reason stated.

8.2 A dynamic participant chooser

For when the assignee depends on the content.

package com.energy.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.ParticipantStepChooser;
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;

/**
 * Chooses the reviewer group based on the product line of the page.
 *
 * Used by a DYNAMIC PARTICIPANT STEP, where a plain Participant Step
 * can't be used because the assignee depends on the payload.
 */
@Component(
        service = ParticipantStepChooser.class,
        property = {
                "chooser.label=Energy - Route to Product Line Reviewers"
        }
)
public class ProductLineReviewerChooser implements ParticipantStepChooser {

    private static final String FALLBACK_GROUP = "energy-content-reviewers";

    @Override
    public String getParticipant(WorkItem workItem,
                                 WorkflowSession workflowSession,
                                 MetaDataMap metaDataMap) throws WorkflowException {

        String payloadPath = workItem.getWorkflowData().getPayload().toString();
        ResourceResolver resolver = workflowSession.adaptTo(ResourceResolver.class);

        Resource content = resolver != null
                ? resolver.getResource(payloadPath + "/jcr:content")
                : null;

        if (content == null) {
            return FALLBACK_GROUP;
        }

        ValueMap properties = content.getValueMap();
        String productLine = properties.get("productLine", String.class);

        // ALWAYS return a GROUP, never an individual user.
        // People go on leave and leave the company; an instance assigned
        // to a departed user sits stuck with no obvious owner.
        if (productLine == null || productLine.isEmpty()) {
            return FALLBACK_GROUP;
        }
        return "energy-reviewers-" + productLine.toLowerCase();

        // NOTE: a returned group that doesn't exist leaves the instance
        // stuck. In production I'd verify it exists and fall back if not.
    }
}

8.3 A workflow launcher configuration

/conf/global/settings/workflow/launcher/config/product-page-approval
<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:jcr="http://www.jcp.org/jcr/1.0"
          xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
    jcr:primaryType="nt:unstructured"

    <!-- SCOPE THE PATH NARROWLY.
         A launcher on /content with this node type fires on EVERY page
         edit across the whole site -- thousands of instances an hour on
         a large site, and /var/workflow/instances fills the disk. -->
    glob="/content/energy/global/en/products/(.*)"

    <!-- The modification happens on jcr:content, not on the cq:Page node -->
    nodetype="cq:PageContent"

    eventType="{Long}16"
    description="Start product page approval on modification"
    enabled="{Boolean}true"

    <!-- RESTRICT THE RUN MODE.
         Without this it runs on PUBLISH too, which is pointless for an
         approval and wastes resources on the tier that should be
         serving pages. Almost every launcher should be author-only. -->
    runModes="[author]"

    <!-- Optional finer control. Evaluated on every matching event,
         so keep it cheap. -->
    condition="jcr:content[@cq:template='/conf/energy/settings/wcm/templates/product-detail-page']"

    workflow="/var/workflow/models/energy/product-page-approval"/>

The two properties that cause production incidents are glob and runModes, and both are commented above for exactly that reason.

8.4 Testing a process step

package com.energy.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.metadata.MetaDataMap;
import io.wcm.testing.mock.aem.junit5.AemContext;
import io.wcm.testing.mock.aem.junit5.AemContextExtension;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mockito;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.when;

@ExtendWith(AemContextExtension.class)
class MarkReviewedProcessTest {

    private final AemContext context = new AemContext();
    private MarkReviewedProcess process;

    private WorkItem workItem;
    private WorkflowSession workflowSession;
    private MetaDataMap metaDataMap;

    @BeforeEach
    void setUp() {
        context.load().json("/workflows/content.json", "/content");
        process = context.registerInjectActivateService(new MarkReviewedProcess());

        workItem        = Mockito.mock(WorkItem.class);
        workflowSession = Mockito.mock(WorkflowSession.class);
        metaDataMap     = Mockito.mock(MetaDataMap.class);

        // The workflow session is how the step gets its resolver
        when(workflowSession.adaptTo(org.apache.sling.api.resource.ResourceResolver.class))
                .thenReturn(context.resourceResolver());
    }

    @Test
    void failsWhenThePayloadNoLongerExists() {
        // A REAL case: the workflow paused for days on a human step
        // and the page was deleted meanwhile.
        mockPayload("/content/does-not-exist", "JCR_PATH");

        assertThrows(WorkflowException.class,
                () -> process.execute(workItem, workflowSession, metaDataMap));
    }

    @Test
    void rejectsAnUnexpectedPayloadType() {
        mockPayload("/content/product", "JCR_UUID");

        assertThrows(WorkflowException.class,
                () -> process.execute(workItem, workflowSession, metaDataMap));
    }

    @Test
    void marksThePageReviewed() throws Exception {
        mockPayload("/content/product", "JCR_PATH");
        when(metaDataMap.get("PROCESS_ARGS", String.class)).thenReturn("compliance");

        process.execute(workItem, workflowSession, metaDataMap);

        String status = context.resourceResolver()
                .getResource("/content/product/jcr:content")
                .getValueMap().get("reviewStatus", String.class);

        assertEquals("compliance-approved", status);
    }

    private void mockPayload(String path, String type) {
        WorkflowData data = Mockito.mock(WorkflowData.class);
        when(data.getPayload()).thenReturn(path);
        when(data.getPayloadType()).thenReturn(type);
        when(workItem.getWorkflowData()).thenReturn(data);
    }
}

Note the tests target the failure paths — a deleted payload, the wrong payload type. The deleted-payload case is specifically a workflow concern, because instances can pause for days and the world changes underneath them.


9. Common Mistakes

The mistakeWhat actually happensThe fix
No process.labelThe step never appears in the editor's dropdownAdd the property
Launcher path too broadThousands of unwanted instances; /var fills the diskScope the glob narrowly
No run mode restrictionThe workflow also runs on publishrunModes="[author]"
Not syncing the modelYour edits have no effectSync in the editor
Assigning steps to individualsStuck forever when that person leavesAssign to groups
Swallowing exceptions in a process stepThe workflow completes as though it succeededThrow WorkflowException
Closing the workflow session's resolverBreaks the rest of the stepOnly close resolvers you opened
Assuming the payload still existsInstances pause for days; content changesNull-check and fail cleanly
Assuming the payload typeIt can be JCR_PATH or JCR_UUIDCheck it
Using a workflow for bulk processingInstance overhead dominates the workSling Jobs, or a transient model
Transient for an auditable processNo record of who approved whatTransient only for automated, non-audited work
No Workflow Purge configured/var/workflow/instances grows without boundConfigure the maintenance task
Slow synchronous external calls in a stepBlocks a workflow thread; risks timeoutExternal Process Step, or dispatch a job
Ignoring the stuck caseInstances accumulate silently for monthsTimeouts, escalation, or a stale-instance report
Using cq:Page in a launcher node typeDoesn't fire — the modification is on jcr:contentcq:PageContent

10. Best Practices

On launchers. Scope the path as narrowly as the requirement allows, always restrict run modes to author, and match on cq:PageContent for page edits. Every unnecessary instance is repository growth you'll pay for later.

On models. Assign to groups, never individuals. Design the stuck case up front — what happens when nobody acts. Use transient only where the audit trail genuinely doesn't matter.

On process steps. Throw rather than swallow, so failures are visible. Don't close the workflow session's resolver. Null-check the payload, because it may have gone. Keep steps fast, and push slow external work out to a job or an External Process Step.

On operations. Make sure Workflow Purge is configured and actually running on 6.5. Monitor the instance count as a leading indicator — it rises long before the disk fills.

On tool choice. Workflow for people and audit trails. Scheduler for time. Sling Job for throughput. Reaching for a workflow because it's familiar is how bulk processing becomes an operational problem.


11. Debugging Tips

The workflow console is the starting point: Tools → Workflow, or /libs/cq/workflow/content/console.html.

TabWhat it tells you
ModelsThe definitions, and where you sync after editing
InstancesWhat's running, and which step each is on
FailuresInstances where a process step threw
LaunchersWhat's configured to start what
ArchiveCompleted instances

When a workflow is stuck, look at which step the instance is on:

  • A Participant Step → it's waiting on a person. Check who it's assigned to, and whether that user or group still exists. An instance assigned to a departed user is the classic case.
  • A Process Step → check error.log and the Failures queue.

From the console you can terminate, retry or advance an instance.

When a workflow doesn't start at all, check the launcher: is it enabled, does the path glob actually match, is the node type right (cq:PageContent, not cq:Page), and does the run mode include this instance?

When your model edits have no effect, you didn't sync. There are two copies and the runtime one is generated from the design one.

When renditions or asset processing fail, remember which platform you're on. On 6.5 that's DAM Update Asset — a workflow, so check Instances and Failures. On Cloud Service it's Asset Compute, so check the asset's processing status and the Processing Profile instead.

ToolAnswers
Tools → Workflow → InstancesWhat's running and where it's stuck
Tools → Workflow → FailuresWhich steps threw
Tools → Workflow → LaunchersWhy it did or didn't start
/var/workflow/instances in CRXDEThe real instance count
error.logProcess step exceptions
/system/console/componentsIs the process step's OSGi component even active

That last one is worth remembering: a process step is an OSGi service, so if it has an unsatisfied @Reference, the component never activates and the step simply doesn't appear in the editor — exactly the file 06 problem in a new place.


12. Performance Optimization

Launcher scope is the highest-impact lever. An unscoped launcher can create orders of magnitude more instances than intended, and every one is repository writes and workflow-engine work.

Transient models where audit isn't needed. No instance nodes, meaningfully faster, no repository growth. This is why DAM Update Asset is transient by default.

Workflow Purge must actually be running on 6.5. Completed instances persist deliberately — something has to remove them.

Keep process steps fast. Each occupies a workflow thread. Slow external calls belong in a job or an External Process Step, and always with timeouts.

Don't use workflows for bulk. Per-instance overhead dominates when the work per item is small.

Watch the instance count as a leading indicator. It grows long before the disk fills, so it's the thing to alert on.


15. Comparison Tables

Workflow vs Scheduler vs Sling Job

WorkflowSchedulerSling Job
TriggerContent event, launcher, manualTime (cron)Code
Human stepsYesNoNo
PersistedYesNoYes
Survives restartYesNoYes
GuaranteedYesNoAt least once
Cluster-awareYesNeeds careYes
Visible to business usersYesNoConsole only
Audit trailYesNoLimited
Use forApprovalsNightly tasksBulk async

Model vs Instance

ModelInstance
What it isThe definitionOne execution
AnalogyA classAn object
Stored at/conf/.../workflow/models + /var/workflow/models/var/workflow/instances
How manyOneOne per run

Normal vs Transient

NormalTransient
Instance nodesCreatedNone
Audit trailYesNo
Human stepsYesNo
SpeedSlowerFaster
Visible in consoleYesNo
Use forApprovalsHigh-volume automation

Step types

StepPurpose
ProcessRuns Java
ParticipantAssigns to a person; pauses
Dynamic ParticipantAssignee chosen by code
Dialog ParticipantPerson fills in a dialog
Or SplitOne branch, by condition
And SplitParallel branches
ContainerRuns another model
GotoJumps to a step
External ProcessWork completed outside AEM

AEM 6.5 vs Cloud Service

6.5Cloud Service
Asset processingDAM Update Asset workflowAsset Compute microservices
Configured viaWorkflow modelProcessing Profiles
Long-running workflowsAcceptableDiscouraged — pods recycle
PurgingYou configure itAutomated
Custom approval workflowsYesYes, unchanged

Next topic

10-Schedulers-Jobs-and-Events — the second half of your syllabus point 19: Sling Scheduler with cron expressions, Sling Jobs and why they're the right answer for bulk processing, event handlers and observation, and the cluster problem — how you stop a scheduled task running on every node.


Topic 09 of the AEM Interview Preparation repository. Teaching style, energy-sector project domain.