AEM Event Handling at a Glance
This guide consolidates AEM event-handling approaches with what each mechanism does, when to use them, how to implement, and copy-paste code samples. Designed for day-to-day development and operations.
AEM exposes several event mechanisms, each suited to a different layer:
- OSGi EventHandler (Event Admin): Publish/consume application/domain events across OSGi components (topic-based).
- JCR EventListener (Observation): Subscribe to low-level repository changes (node/property
created/removed/changed) under a path. - Sling ResourceChangeListener: Listen to Sling resource-level create/update/delete events with path/glob filters.
- Sling Jobs (JobManager): Asynchronous job queue with retry & clustering—ideal to offload heavy work from listeners/steps.
- TransportHandler (Replication): Implement custom delivery channels for activation/deactivation payloads.
- Workflow Launchers: Declaratively trigger workflow models on repository changes for multi-step business processes.
When to Use Which
EventHandler – Decoupled intra-app communication (domain events).
JCR EventListener – Precise, path-scoped detection of node/property changes.
ResourceChangeListener – Sling-native resource events with glob/path filtering.
Sling Jobs – Offload heavy/Retryable work with backoff and cluster-aware processing.
Workflow Launchers – Business processes (multi-step, approvals) started by repository changes.
TransportHandler – Custom replication transport to external systems (CDN/API/message bus).
1) OSGi Event Admin – EventHandler
What it is: Topic-based pub/sub via OSGi Event Admin. Producers post events with a string topic and a map of properties; consumers register EventHandlers for topics. AEM exposed some OOTB topics like: ReplicationAction.EVENT_TOPIC
Typical use cases:
- Announce domain events: “preview generated”, “asset validated”, “cache must purge”.
- Bridge external signals into AEM as events (e.g., from a message broker via a connector).
Implementation (Consumer)
package com.example.aem.events;
import org.osgi.service.event.Event;
import org.osgi.service.event.EventConstants;
import org.osgi.service.event.EventHandler;
import org.osgi.service.component.annotations.Component;
@Component(
service = EventHandler.class,
immediate = true,
property = {
EventConstants.EVENT_TOPIC + "=com/example/aem/asset/validated",
EventConstants.EVENT_TOPIC + "=com/example/aem/cache/purge"
}
)
public class AssetValidatedEventHandler implements EventHandler {
@Override
public void handleEvent(Event event) {
String topic = (String) event.getProperty(EventConstants.EVENT_TOPIC);
String path = (String) event.getProperty("path");
if ("com/example/aem/asset/validated".equals(topic)) {
// TODO: business logic
} else if ("com/example/aem/cache/purge".equals(topic)) {
// TODO: cache purge
}
}
}
Posting an event (Producer)
package com.example.aem.events;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.event.Event;
import org.osgi.service.event.EventAdmin;
import java.util.Dictionary;
import java.util.Hashtable;
@Component(service = AssetEventProducer.class)
public class AssetEventProducer {
@Reference
private EventAdmin eventAdmin;
public void publishValidated(String assetPath) {
Dictionary<String, Object> props = new Hashtable<>();
props.put("path", assetPath);
eventAdmin.postEvent(new Event("com/example/aem/asset/validated", props));
}
}
Tips
- Keep topics namespaced (
com/yourco/app/...) and stable. - Use
postEvent(async) for most cases;sendEvent(sync) only if necessary. - Avoid excessive events; consider Sling Jobs for heavy/Retryable work.
2) JCR Observation – EventListener
What it is: Subscribe to repository changes (node/property added/removed/changed) under a path using JCR Observation.
Typical use cases
- React the moment a node or binary is created/updated.
- Maintain derived content (e.g., index nodes or metadata propagation).
package com.example.aem.jcr;
import java.util.Iterator;
import javax.jcr.Session;
import javax.jcr.observation.Event;
import javax.jcr.observation.EventListener;
import javax.jcr.observation.ObservationManager;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.apache.sling.api.resource.LoginException;
import org.osgi.service.component.annotations.*;
@Component(service = EventListener.class, immediate = true)
public class DamAssetCreateListener implements EventListener {
private static final String ROOT = "/content/dam/project";
@Reference
private ResourceResolverFactory rrf;
private ResourceResolver resolver;
private ObservationManager observationManager;
@Activate
protected void activate() throws Exception {
// Use a service user with minimal privileges
resolver = rrf.getServiceResourceResolver(
java.util.Collections.singletonMap(ResourceResolverFactory.SUBSERVICE, "dam-listener")
);
Session session = resolver.adaptTo(Session.class);
observationManager = session.getWorkspace().getObservationManager();
int eventTypes = Event.NODE_ADDED | Event.PROPERTY_CHANGED;
// deep = true, no UUID filter, no node type filter, no local = true
observationManager.addEventListener(this, eventTypes, ROOT, true, null, null, true);
}
@Deactivate
protected void deactivate() {
try {
if (observationManager != null) {
observationManager.removeEventListener(this);
}
} catch (Exception ignored) {}
if (resolver != null) {
resolver.close();
}
}
@Override
public void onEvent(Iterator<Event> events) {
events.forEachRemaining(ev -> {
try {
String path = ev.getPath();
int type = ev.getType(); // Event.NODE_ADDED, etc.
// TODO: business logic (e.g., tag assets, enqueue processing)
} catch (Exception e) {
// log and continue
}
});
}
}
Tips
- Scope the path tightly to avoid a flood of events.
- Use service users and close the
ResourceResolver. - For high-level resource events, consider Sling’s
ResourceChangeListener(optional), but JCR observation gives exact low-level control.
3) Sling Resource Observation – ResourceChangeListener
What it is: ‘ResourceChangeListener’ is a Sling-level API that listens to resource events (added, changed, removed) across one or more paths, with support for glob patterns and change types. It’s lighter and more Sling-native than raw JCR Observation, and avoids dealing directly with JCR ‘Event’ types.
Typical use cases
- Listen for content/resource-level changes under specific Sling paths (e.g., ‘/content/dam/project/**’).
- React to create/update/delete events without needing low-level JCR specifics.
- Preferable when working primarily with the Sling Resource API.
package com.example.aem.sling;
import org.apache.sling.api.resource.observation.ResourceChange;
import org.apache.sling.api.resource.observation.ResourceChangeListener;
import org.osgi.service.component.annotations.Component;
import java.util.List;
import java.util.Set;
/**
* Listens for resource changes under /content/dam/project, using Sling events.
*/
@Component(
service = ResourceChangeListener.class,
property = {
// Path(s) to watch; supports multiple values
ResourceChangeListener.PATHS + "=/content/dam/project",
// Use glob patterns if needed:
// ResourceChangeListener.GLOB_PATHS + "=/content/dam/project/**",
// Limit change types:
ResourceChangeListener.CHANGE_TYPES + "=ADDED",
ResourceChangeListener.CHANGE_TYPES + "=CHANGED",
ResourceChangeListener.CHANGE_TYPES + "=REMOVED"
}
)
public class DamResourceChangeListener implements ResourceChangeListener {
@Override
public void onChange(List<ResourceChange> changes) {
for (ResourceChange change : changes) {
String path = change.getPath();
ResourceChange.ChangeType type = change.getType();
// TODO: react to ADDED/CHANGED/REMOVED; offload heavy work to Sling Jobs
}
}
}
Tips
- Prefer ‘GLOB_PATHS’ for fine-grained scoping (e.g., only ‘*/.jpg’).
- This is Sling-native work with ‘ResourceResolver’ and resources rather than JCR ‘Session’.
- Keep the ‘onChange’ handler fast – enqueue heavy tasks to Sling Jobs to avoid blocking the event thread.
4) Background Processing & Retry — Sling Jobs (JobManager)
What it is: Sling Jobs provide a built-in asynchronous job queue with retry, deduplication, and distributed processing across a cluster. You can add jobs to a topic and implement a job consumer to process them. Perfect companion for ‘EventListener’ or ‘ResourceChangeListener’ when work is heavy, slow, or may fail transiently.
Typical use cases
- Offload heavy processing triggered by repository changes (renditions, metadata enrichment, external API calls).
- Implement retryable tasks with backoff.
- Decouple listeners from execution; keep listeners thin.
Enqueue a Job (Producer)
package com.example.aem.jobs;
import org.apache.sling.event.jobs.JobManager;
import org.apache.sling.event.jobs.Job;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import java.util.HashMap;
import java.util.Map;
@Component(service = JobProducer.class)
public class JobProducer {
public static final String TOPIC = "com/example/aem/dam/preview";
@Reference
private JobManager jobManager;
public void enqueuePreviewJob(String assetPath) {
Map<String, Object> props = new HashMap<>();
props.put("path", assetPath);
Job job = jobManager.addJob(TOPIC, props);
// Optionally log job.getId()
}
}
Call ‘enqueuePreviewJob(…)’ from your ‘EventListener’ / ‘ResourceChangeListener’ / Workflow step.
Consume a Job (Consumer)
package com.example.aem.jobs;
import org.apache.sling.event.jobs.consumer.JobConsumer;
import org.apache.sling.event.jobs.consumer.JobConsumer.JobResult;
import org.apache.sling.event.jobs.Job;
import org.osgi.service.component.annotations.Component;
/**
* Consumes preview generation jobs.
*/
@Component(
service = JobConsumer.class,
property = {
JobConsumer.PROPERTY_TOPICS + "=" + JobProducer.TOPIC
}
)
public class PreviewJobConsumer implements JobConsumer {
@Override
public JobResult process(Job job) {
String path = (String) job.getProperty("path");
try {
// TODO: generate preview; long-running allowed
return JobResult.OK;
} catch (TransientException t) {
return JobResult.RETRY; // will be retried per Sling job configs
} catch (Exception e) {
return JobResult.FAILED; // logged; consider alerting
}
}
}
Tips
- Use topic namespaces (‘com/yourco/…’) to keep job routing clear.
- Return ‘RETRY’ for transient failures; configure max retries/backoff via OSGi.
- Jobs are cluster-aware only one consumer runs a given job across instances.
5) Replication – Custom TransportHandler
What it is: AEM replication activates/deactivates content to publish (or other targets). A TransportHandler defines how payloads are delivered. Built‑in HTTP transports cover standard publish instances; a custom handler lets you deliver to non‑standard endpoints (CDN APIs, message queues, partner systems).
Typical use cases
- Push activation/deactivation to an external API instead of a publish instance.
- Sign payloads or transform replication content before delivery.
package com.example.aem.replication;
import com.day.cq.replication.TransportHandler;
import com.day.cq.replication.AgentConfig;
import com.day.cq.replication.ReplicationTransaction;
import com.day.cq.replication.ReplicationActionType;
import com.day.cq.replication.ReplicationContent;
import com.day.cq.replication.TransportException;
import org.osgi.service.component.annotations.Component;
/**
* Delivers replication transactions to a custom HTTP endpoint.
*/
@Component(service = TransportHandler.class)
public class CustomHttpTransportHandler implements TransportHandler {
/**
* Decide if this handler should handle the given agent.
* Common pattern: check scheme or config name.
*/
@Override
public boolean canHandle(AgentConfig agentConfig) {
String uri = agentConfig.getTransportURI(); // e.g., "custom-http://api.example.com"
return uri != null && uri.startsWith("custom-http:");
}
/**
* Deliver the replication transaction.
*/
@Override
public void deliver(ReplicationTransaction tx) throws TransportException {
try {
ReplicationActionType type = tx.getAction().getType(); // ACTIVATE/DEACTIVATE/DELETE
ReplicationContent content = tx.getContent();
// Read the payload stream (for ACTIVATE) or path (for other actions)
// InputStream is = content.getInputStream(); // if present
String path = tx.getAction().getPath();
// TODO: call your external endpoint (HTTP client), include path, action type,
// and bytes if needed. Handle retries, timeouts, idempotency, auth.
} catch (Exception e) {
throw new TransportException("Delivery failed", e);
}
}
/**
* Optional: cancel ongoing deliveries (if your handler supports long‑running calls).
*/
@Override
public void cancel() {
// TODO: abort/close resources if needed
}
}
Setup
- Create a Replication Agent (Tools > Deployment > Replication > Agents on author).
- Set the agent’s transport URI (e.g. ‘custom-http://api.example.com’) so ‘canHandle(…)’ selects your handler.
- Secure with service users, secrets in OSGi configs, and TLS.
Tips
- Keep delivery idempotent replication may retry on transient failures.
- Log action type + path for operational insight.
- For very large payloads, consider streaming and chunking.
6) Workflow Launchers
What they are: A launcher starts a workflow model automatically when repository changes occur (node created/modified/removed) under a path and matching filters. They’re declarative and can be managed by admins – ideal for content processing and editorial flows.
Typical use cases
- DAM: On asset upload > run “generate renditions & metadata” workflow.
- Content: On node modified > run “publish request with approvals”.
Create a Workflow Process Step
package com.example.aem.workflow;
import com.adobe.granite.workflow.exec.WorkItem;
import com.adobe.granite.workflow.exec.WorkflowSession;
import com.adobe.granite.workflow.metadata.MetaDataMap;
import com.adobe.granite.workflow.WorkflowException;
import com.adobe.granite.workflow.exec.WorkflowProcess;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.osgi.service.component.annotations.*;
@Component(
service = WorkflowProcess.class,
property = {
"process.label=Generate Preview"
}
)
public class GeneratePreviewProcess implements WorkflowProcess {
@Reference
private ResourceResolverFactory rrf;
@Override
public void execute(WorkItem workItem, WorkflowSession workflowSession, MetaDataMap args)
throws WorkflowException {
String payloadPath = workItem.getWorkflowData().getPayload().toString();
try (ResourceResolver rr = rrf.getServiceResourceResolver(
java.util.Collections.singletonMap(ResourceResolverFactory.SUBSERVICE, "workflow-process"))) {
// TODO: generate preview for payloadPath
} catch (Exception e) {
throw new WorkflowException("Preview generation failed for " + payloadPath, e);
}
}
}
Create a Workflow Model
- Tools > Workflow > Models > Create a model.
- Add your “Generate Preview” process step.
- Save & activate the model.
Configure a Launcher
- Tools > Workflow > Launchers > Create.
- Event type: e.g., “Node created”.
- Path: e.g., ‘/content/dam/project(/.*)?’.
- Condition/filter: e.g., ‘jcr:primaryType=dam:Asset’ or ‘glob=*/.jpg’.
- Workflow: select your model.
- Save and enable.
Tips
- Prefer specific filters to avoid running workflows on unrelated content.
- Keep steps idempotent; launchers may fire on reprocessing or restore.
- For heavy jobs, offload processing to Sling Jobs inside the workflow step.
Differences Summarized
Trigger source: EventHandler (app events); JCR EventListener (repository low-level); ResourceChangeListener (resource-level); TransportHandler (replication delivery); Workflow Launcher (declarative process).
Configuration Focus: EventHandler via OSGi properties; EventListener via ObservationManager; ResourceChangeListener via component properties; TransportHandler via replication agent config; Launchers
via admin UI.
Best fit: EventHandler for decoupled business events; EventListener for exact JCR detection; ResourceChangeListener for Sling resource changes; TransportHandler for custom delivery; Launchers for multi-step processes.



Post Comment