> 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.

# 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:

```text
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"]

| Piece        | What it is                                                                 |
| ------------ | -------------------------------------------------------------------------- |
| **Model**    | The **definition** — the steps and their order. Like a class.              |
| **Instance** | One **running execution** of a model. Like an object.                      |
| **Launcher** | A rule that **starts** a workflow when something happens in the repository |
| **Payload**  | The **content** the workflow is acting on — usually a page or asset path   |
| **Step**     | One 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.

| What                | Where                                                |
| ------------------- | ---------------------------------------------------- |
| **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 type                    | What it does                                                     |
| ---------------------------- | ---------------------------------------------------------------- |
| **Process Step**             | Runs Java code. The one developers write.                        |
| **Participant Step**         | Assigns to a user or group; appears in their Inbox and **waits** |
| **Dynamic Participant Step** | Same, but the assignee is chosen at runtime by code              |
| **Dialog Participant Step**  | A participant step where the person fills in a dialog            |
| **Or Split**                 | Conditional branching — one path taken                           |
| **And Split**                | Parallel branches — all paths taken                              |
| **Container Step**           | Runs another workflow model inside this one                      |
| **Goto Step**                | Jumps to another step — conditional loops                        |
| **External Process Step**    | For 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.

```java
@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:

| Parameter         | What it gives you                                        |
| ----------------- | -------------------------------------------------------- |
| `WorkItem`        | The current step, and the **payload**                    |
| `WorkflowSession` | The workflow API — and a way to get a `ResourceResolver` |
| `MetaDataMap`     | Arguments configured on the step in the editor           |

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

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

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

```java
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:**

```java
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:**

| Setting        | Purpose                                  |
| -------------- | ---------------------------------------- |
| **Event Type** | Created, Modified, Removed               |
| **Node Type**  | e.g. `cq:PageContent`, `dam:Asset`       |
| **Path**       | A regex — which part of the repository   |
| **Condition**  | An optional expression for finer control |
| **Workflow**   | Which model to run                       |
| **Run Modes**  | **author** and/or **publish**            |
| **Activate**   | Enabled 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.

|                                 | Normal                        | Transient                        |
| ------------------------------- | ----------------------------- | -------------------------------- |
| Instance nodes created          | Yes                           | **No**                           |
| Repository growth               | Yes                           | None                             |
| Speed                           | Slower                        | **Faster**                       |
| History / audit trail           | **Yes**                       | **No**                           |
| Visible in the Workflow console | Yes                           | No                               |
| Can have human steps            | Yes                           | **No**                           |
| Use for                         | Approvals, anything auditable | High-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:

| Workflow                                         | What it does                                             |
| ------------------------------------------------ | -------------------------------------------------------- |
| **DAM Update Asset**                             | Renditions, metadata extraction, smart tags on upload    |
| **Request for Activation**                       | Approval before publishing, when the author lacks rights |
| **Request for Deletion**                         | Approval before deleting                                 |
| **Scheduled Activation / Deactivation**          | Waits until a set time, then publishes or unpublishes    |
| **Move / Delete DAM Asset**                      | Handles references when assets move                      |
| **Translation workflows**                        | Created by translation projects                          |
| **Publish Example / Approve for Adobe Campaign** | Sample 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.

|                           | **Workflow**                         | **Scheduler**                 | **Sling Job**          |
| ------------------------- | ------------------------------------ | ----------------------------- | ---------------------- |
| Triggered by              | Content event, launcher, or manually | **Time** (cron)               | Code, programmatically |
| Human steps               | **Yes**                              | No                            | No                     |
| Persisted                 | Yes (`/var`)                         | **No**                        | Yes                    |
| Survives a restart        | Yes                                  | **No**                        | **Yes**                |
| Guaranteed to run         | Yes                                  | No                            | **At least once**      |
| Cluster-aware             | Yes                                  | Needs care                    | **Yes**                |
| Visible to business users | **Yes** — Inbox, console             | No                            | Job queue console only |
| Audit trail               | **Yes**                              | No                            | Limited                |
| Use for                   | Approvals, business process          | Nightly report, cache warm-up | Bulk 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

```java
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.

```java
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

```text
/conf/global/settings/workflow/launcher/config/product-page-approval
```

```xml
<?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

```java
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 mistake                               | What actually happens                                  | The fix                                          |
| ----------------------------------------- | ------------------------------------------------------ | ------------------------------------------------ |
| No `process.label`                        | The step never appears in the editor's dropdown        | Add the property                                 |
| Launcher path too broad                   | Thousands of unwanted instances; `/var` fills the disk | Scope the `glob` narrowly                        |
| No run mode restriction                   | The workflow also runs on **publish**                  | `runModes="[author]"`                            |
| Not syncing the model                     | Your edits have no effect                              | Sync in the editor                               |
| Assigning steps to individuals            | Stuck forever when that person leaves                  | Assign to **groups**                             |
| Swallowing exceptions in a process step   | The workflow completes as though it succeeded          | Throw `WorkflowException`                        |
| Closing the workflow session's resolver   | Breaks the rest of the step                            | Only close resolvers you opened                  |
| Assuming the payload still exists         | Instances pause for days; content changes              | Null-check and fail cleanly                      |
| Assuming the payload type                 | It can be `JCR_PATH` or `JCR_UUID`                     | Check it                                         |
| Using a workflow for bulk processing      | Instance overhead dominates the work                   | Sling Jobs, or a transient model                 |
| Transient for an auditable process        | No record of who approved what                         | Transient only for automated, non-audited work   |
| No Workflow Purge configured              | `/var/workflow/instances` grows without bound          | Configure the maintenance task                   |
| Slow synchronous external calls in a step | Blocks a workflow thread; risks timeout                | External Process Step, or dispatch a job         |
| Ignoring the stuck case                   | Instances accumulate silently for months               | Timeouts, escalation, or a stale-instance report |
| Using `cq:Page` in a launcher node type   | Doesn't fire — the modification is on `jcr:content`    | `cq: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`.

| Tab           | What it tells you                                 |
| ------------- | ------------------------------------------------- |
| **Models**    | The definitions, and where you sync after editing |
| **Instances** | What's running, and which step each is on         |
| **Failures**  | Instances where a process step threw              |
| **Launchers** | What's configured to start what                   |
| **Archive**   | Completed 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.

| Tool                               | Answers                                          |
| ---------------------------------- | ------------------------------------------------ |
| Tools → Workflow → Instances       | What's running and where it's stuck              |
| Tools → Workflow → Failures        | Which steps threw                                |
| Tools → Workflow → Launchers       | Why it did or didn't start                       |
| `/var/workflow/instances` in CRXDE | The real instance count                          |
| `error.log`                        | Process step exceptions                          |
| `/system/console/components`       | Is 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**

|                           | Workflow                        | Scheduler       | Sling Job         |
| ------------------------- | ------------------------------- | --------------- | ----------------- |
| Trigger                   | Content event, launcher, manual | **Time** (cron) | Code              |
| Human steps               | **Yes**                         | No              | No                |
| Persisted                 | Yes                             | **No**          | Yes               |
| Survives restart          | Yes                             | No              | **Yes**           |
| Guaranteed                | Yes                             | No              | **At least once** |
| Cluster-aware             | Yes                             | Needs care      | **Yes**           |
| Visible to business users | **Yes**                         | No              | Console only      |
| Audit trail               | **Yes**                         | No              | Limited           |
| Use for                   | Approvals                       | Nightly tasks   | Bulk async        |

**Model vs Instance**

|            | Model                                                | Instance                  |
| ---------- | ---------------------------------------------------- | ------------------------- |
| What it is | The definition                                       | One execution             |
| Analogy    | A class                                              | An object                 |
| Stored at  | `/conf/.../workflow/models` + `/var/workflow/models` | `/var/workflow/instances` |
| How many   | One                                                  | One per run               |

**Normal vs Transient**

|                    | Normal    | Transient              |
| ------------------ | --------- | ---------------------- |
| Instance nodes     | Created   | **None**               |
| Audit trail        | **Yes**   | No                     |
| Human steps        | Yes       | **No**                 |
| Speed              | Slower    | Faster                 |
| Visible in console | Yes       | No                     |
| Use for            | Approvals | High-volume automation |

**Step types**

| Step                | Purpose                         |
| ------------------- | ------------------------------- |
| Process             | Runs Java                       |
| Participant         | Assigns to a person; **pauses** |
| Dynamic Participant | Assignee chosen by code         |
| Dialog Participant  | Person fills in a dialog        |
| Or Split            | One branch, by condition        |
| And Split           | Parallel branches               |
| Container           | Runs another model              |
| Goto                | Jumps to a step                 |
| External Process    | Work completed outside AEM      |

**AEM 6.5 vs Cloud Service**

|                           | 6.5                           | Cloud Service                   |
| ------------------------- | ----------------------------- | ------------------------------- |
| Asset processing          | DAM Update Asset **workflow** | **Asset Compute microservices** |
| Configured via            | Workflow model                | **Processing Profiles**         |
| Long-running workflows    | Acceptable                    | **Discouraged** — pods recycle  |
| Purging                   | You configure it              | **Automated**                   |
| Custom approval workflows | Yes                           | **Yes, 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._
