plugins/aem/cloud-service/skills/content-distribution/sling-distribution/SKILL.md
Monitor and react to content distribution events using Sling Distribution API (org.apache.sling.distribution). Covers distribution event handling, queue monitoring, and distribution lifecycle tracking.
npx skillsauth add adobe/skills sling-distributionInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Monitor and react to content distribution lifecycle events using the Sling Distribution API.
Use Sling Distribution event handling for:
For programmatic publishing, use the Replication API instead.
Javadoc: https://developer.adobe.com/experience-manager/reference-materials/cloud-service/javadoc/org/apache/sling/distribution/package-summary.html
Key Packages:
org.apache.sling.distribution - Core distribution interfacesorg.apache.sling.distribution.event - Event topics and propertiesorg.apache.sling.distribution.queue - Queue monitoringSling Distribution is the underlying transport mechanism for content replication in AEM Cloud Service. When you use the Replication API (Replicator.replicate()), Sling Distribution handles:
Replication API Call
↓
[AGENT_PACKAGE_CREATED] - Package assembled
↓
[AGENT_PACKAGE_QUEUED] - Package added to queue
↓
[AGENT_PACKAGE_DISTRIBUTED] - Package sent to pipeline
↓
[IMPORTER_PACKAGE_IMPORTED] - Package imported on target tier
OR
[AGENT_PACKAGE_DROPPED] - Package failed and was removed
Sling Distribution fires events at each stage of the distribution lifecycle:
| Event Topic | When It Fires | Use Case |
|-------------|---------------|----------|
| AGENT_PACKAGE_CREATED | After package creation | Track what's being published |
| AGENT_PACKAGE_QUEUED | After package is queued | Monitor queue depth |
| AGENT_PACKAGE_DISTRIBUTED | After successful distribution | Trigger post-publish actions |
| AGENT_PACKAGE_DROPPED | When package fails and is dropped | Handle failures, alert on-call |
| IMPORTER_PACKAGE_IMPORTED | After successful import on target | Confirm content is live |
import org.apache.sling.distribution.event.DistributionEventTopics;
// Event topic strings
DistributionEventTopics.AGENT_PACKAGE_CREATED // Package created
DistributionEventTopics.AGENT_PACKAGE_QUEUED // Package queued
DistributionEventTopics.AGENT_PACKAGE_DISTRIBUTED // Package distributed
DistributionEventTopics.AGENT_PACKAGE_DROPPED // Package dropped
DistributionEventTopics.IMPORTER_PACKAGE_IMPORTED // Package imported
import org.apache.sling.distribution.event.DistributionEventTopics;
import org.apache.sling.distribution.event.DistributionEventProperties;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.event.Event;
import org.osgi.service.event.EventHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Component(
service = EventHandler.class,
property = {
org.osgi.service.event.EventConstants.EVENT_TOPIC + "=" +
DistributionEventTopics.AGENT_PACKAGE_CREATED,
org.osgi.service.event.EventConstants.EVENT_TOPIC + "=" +
DistributionEventTopics.AGENT_PACKAGE_QUEUED,
org.osgi.service.event.EventConstants.EVENT_TOPIC + "=" +
DistributionEventTopics.AGENT_PACKAGE_DISTRIBUTED,
org.osgi.service.event.EventConstants.EVENT_TOPIC + "=" +
DistributionEventTopics.AGENT_PACKAGE_DROPPED,
org.osgi.service.event.EventConstants.EVENT_TOPIC + "=" +
DistributionEventTopics.IMPORTER_PACKAGE_IMPORTED
}
)
public class DistributionEventLogger implements EventHandler {
private static final Logger LOG = LoggerFactory.getLogger(DistributionEventLogger.class);
@Override
public void handleEvent(Event event) {
String topic = event.getTopic();
// Extract event properties
String componentName = (String) event.getProperty(
DistributionEventProperties.DISTRIBUTION_COMPONENT_NAME
);
String componentKind = (String) event.getProperty(
DistributionEventProperties.DISTRIBUTION_COMPONENT_KIND
);
String distributionType = (String) event.getProperty(
DistributionEventProperties.DISTRIBUTION_TYPE
);
String packageId = (String) event.getProperty(
DistributionEventProperties.DISTRIBUTION_PACKAGE_ID
);
String[] paths = (String[]) event.getProperty(
DistributionEventProperties.DISTRIBUTION_PATHS
);
Long timestamp = (Long) event.getProperty(
DistributionEventProperties.DISTRIBUTION_ENQUEUE_TIMESTAMP
);
LOG.info("Distribution Event: topic={}, component={}, type={}, packageId={}, paths={}, timestamp={}",
topic, componentName, distributionType, packageId,
paths != null ? String.join(",", paths) : "null",
timestamp
);
}
}
Every distribution event contains these properties:
| Property | Type | Description |
|----------|------|-------------|
| DISTRIBUTION_COMPONENT_NAME | String | Name of component generating the event |
| DISTRIBUTION_COMPONENT_KIND | String | Kind of component (agent, importer, etc.) |
| DISTRIBUTION_TYPE | String | Type of distribution request (ADD, DELETE, etc.) |
| DISTRIBUTION_PACKAGE_ID | String | Unique package identifier |
| DISTRIBUTION_PATHS | String[] | Content paths being distributed |
| DISTRIBUTION_DEEP_PATHS | String[] | Deep paths (full subtree) |
| DISTRIBUTION_ENQUEUE_TIMESTAMP | Long | When item was enqueued (milliseconds) |
import org.apache.sling.distribution.event.DistributionEventProperties;
@Override
public void handleEvent(Event event) {
// Component information
String componentName = (String) event.getProperty(
DistributionEventProperties.DISTRIBUTION_COMPONENT_NAME
);
String componentKind = (String) event.getProperty(
DistributionEventProperties.DISTRIBUTION_COMPONENT_KIND
);
// Distribution details
String type = (String) event.getProperty(
DistributionEventProperties.DISTRIBUTION_TYPE
);
String packageId = (String) event.getProperty(
DistributionEventProperties.DISTRIBUTION_PACKAGE_ID
);
// Content paths
String[] paths = (String[]) event.getProperty(
DistributionEventProperties.DISTRIBUTION_PATHS
);
String[] deepPaths = (String[]) event.getProperty(
DistributionEventProperties.DISTRIBUTION_DEEP_PATHS
);
// Timing
Long enqueueTime = (Long) event.getProperty(
DistributionEventProperties.DISTRIBUTION_ENQUEUE_TIMESTAMP
);
}
import org.apache.sling.distribution.event.DistributionEventTopics;
@Component(
service = EventHandler.class,
property = {
org.osgi.service.event.EventConstants.EVENT_TOPIC + "=" +
DistributionEventTopics.AGENT_PACKAGE_DROPPED
}
)
public class DistributionFailureAlertHandler implements EventHandler {
private static final Logger LOG = LoggerFactory.getLogger(
DistributionFailureAlertHandler.class
);
@Reference
private AlertService alertService; // Custom alert service
@Override
public void handleEvent(Event event) {
String packageId = (String) event.getProperty(
DistributionEventProperties.DISTRIBUTION_PACKAGE_ID
);
String[] paths = (String[]) event.getProperty(
DistributionEventProperties.DISTRIBUTION_PATHS
);
LOG.error("Distribution package dropped: packageId={}, paths={}",
packageId, String.join(",", paths));
// Send alert to operations team
alertService.sendAlert(
"CRITICAL: Distribution Failed",
String.format("Package %s was dropped. Paths: %s",
packageId, String.join(",", paths))
);
}
}
@Component(
service = EventHandler.class,
property = {
org.osgi.service.event.EventConstants.EVENT_TOPIC + "=" +
DistributionEventTopics.IMPORTER_PACKAGE_IMPORTED
}
)
public class CdnCacheWarmingHandler implements EventHandler {
private static final Logger LOG = LoggerFactory.getLogger(
CdnCacheWarmingHandler.class
);
@Reference
private HttpClient httpClient;
@Override
public void handleEvent(Event event) {
String[] paths = (String[]) event.getProperty(
DistributionEventProperties.DISTRIBUTION_PATHS
);
String distributionType = (String) event.getProperty(
DistributionEventProperties.DISTRIBUTION_TYPE
);
// Only warm cache on ADD (activation)
if ("ADD".equals(distributionType) && paths != null) {
for (String path : paths) {
warmCache(path);
}
}
}
private void warmCache(String path) {
try {
// Convert JCR path to public URL
String publicUrl = "https://www.example.com" +
path.replace("/content/mysite", "") + ".html";
LOG.info("Warming CDN cache for: {}", publicUrl);
HttpGet request = new HttpGet(publicUrl);
httpClient.execute(request);
} catch (Exception e) {
LOG.error("Cache warming failed for path: " + path, e);
}
}
}
@Component(
service = EventHandler.class,
property = {
org.osgi.service.event.EventConstants.EVENT_TOPIC + "=" +
DistributionEventTopics.AGENT_PACKAGE_CREATED,
org.osgi.service.event.EventConstants.EVENT_TOPIC + "=" +
DistributionEventTopics.AGENT_PACKAGE_DISTRIBUTED,
org.osgi.service.event.EventConstants.EVENT_TOPIC + "=" +
DistributionEventTopics.AGENT_PACKAGE_DROPPED
}
)
public class DistributionAnalyticsHandler implements EventHandler {
@Reference
private AnalyticsService analyticsService;
@Override
public void handleEvent(Event event) {
String topic = event.getTopic();
String packageId = (String) event.getProperty(
DistributionEventProperties.DISTRIBUTION_PACKAGE_ID
);
String[] paths = (String[]) event.getProperty(
DistributionEventProperties.DISTRIBUTION_PATHS
);
Long timestamp = (Long) event.getProperty(
DistributionEventProperties.DISTRIBUTION_ENQUEUE_TIMESTAMP
);
// Track metrics
if (DistributionEventTopics.AGENT_PACKAGE_CREATED.equals(topic)) {
analyticsService.trackEvent("distribution.package.created",
Map.of("packageId", packageId, "pathCount", paths.length));
}
else if (DistributionEventTopics.AGENT_PACKAGE_DISTRIBUTED.equals(topic)) {
long duration = System.currentTimeMillis() - timestamp;
analyticsService.trackEvent("distribution.package.distributed",
Map.of("packageId", packageId, "duration", duration));
}
else if (DistributionEventTopics.AGENT_PACKAGE_DROPPED.equals(topic)) {
analyticsService.trackEvent("distribution.package.failed",
Map.of("packageId", packageId, "paths", String.join(",", paths)));
}
}
}
@Component(
service = EventHandler.class,
property = {
org.osgi.service.event.EventConstants.EVENT_TOPIC + "=" +
DistributionEventTopics.IMPORTER_PACKAGE_IMPORTED
}
)
public class ProductionPublishNotificationHandler implements EventHandler {
@Reference
private SlackService slackService;
@Reference
private SlingSettingsService slingSettings;
@Override
public void handleEvent(Event event) {
// Only notify for production environment
if (!slingSettings.getRunModes().contains("prod")) {
return;
}
String[] paths = (String[]) event.getProperty(
DistributionEventProperties.DISTRIBUTION_PATHS
);
String componentName = (String) event.getProperty(
DistributionEventProperties.DISTRIBUTION_COMPONENT_NAME
);
// Only notify for publish agent (not preview)
if (!"publish".equals(componentName)) {
return;
}
String message = String.format(
":rocket: Content published to production: %s",
String.join(", ", paths)
);
slackService.postToChannel("#content-releases", message);
}
}
import javax.jcr.Node;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;
@Component(
service = EventHandler.class,
property = {
org.osgi.service.event.EventConstants.EVENT_TOPIC + "=" +
DistributionEventTopics.AGENT_PACKAGE_CREATED,
org.osgi.service.event.EventConstants.EVENT_TOPIC + "=" +
DistributionEventTopics.AGENT_PACKAGE_QUEUED,
org.osgi.service.event.EventConstants.EVENT_TOPIC + "=" +
DistributionEventTopics.AGENT_PACKAGE_DISTRIBUTED,
org.osgi.service.event.EventConstants.EVENT_TOPIC + "=" +
DistributionEventTopics.AGENT_PACKAGE_DROPPED,
org.osgi.service.event.EventConstants.EVENT_TOPIC + "=" +
DistributionEventTopics.IMPORTER_PACKAGE_IMPORTED
}
)
public class DistributionAuditHandler implements EventHandler {
@Reference
private ResourceResolverFactory resolverFactory;
@Override
public void handleEvent(Event event) {
try (ResourceResolver resolver = getServiceResolver()) {
String topic = event.getTopic();
String packageId = (String) event.getProperty(
DistributionEventProperties.DISTRIBUTION_PACKAGE_ID
);
String[] paths = (String[]) event.getProperty(
DistributionEventProperties.DISTRIBUTION_PATHS
);
Long timestamp = (Long) event.getProperty(
DistributionEventProperties.DISTRIBUTION_ENQUEUE_TIMESTAMP
);
// Create audit log entry under /var/audit/distribution
String auditPath = "/var/audit/distribution/" +
System.currentTimeMillis();
Node auditNode = resolver.getResource("/var/audit/distribution")
.adaptTo(Node.class)
.addNode(String.valueOf(System.currentTimeMillis()), "nt:unstructured");
auditNode.setProperty("topic", topic);
auditNode.setProperty("packageId", packageId);
auditNode.setProperty("paths", paths);
auditNode.setProperty("timestamp", timestamp);
auditNode.setProperty("auditTime", System.currentTimeMillis());
resolver.commit();
} catch (Exception e) {
LOG.error("Failed to create audit log", e);
}
}
private ResourceResolver getServiceResolver() throws Exception {
Map<String, Object> param = Map.of(
ResourceResolverFactory.SUBSERVICE, "distributionAuditor"
);
return resolverFactory.getServiceResourceResolver(param);
}
}
When monitoring events, the DISTRIBUTION_TYPE property indicates the type of operation:
| Request Type | Description | When Used |
|--------------|-------------|-----------|
| ADD | Content is being added/activated | Normal publishing |
| DELETE | Content is being deleted | Unpublishing, deletion |
| PULL | Content is being pulled from target | Reverse replication |
| INVALIDATE | Cache invalidation only | CDN purge |
| TEST | Connection test | Health checks |
@Override
public void handleEvent(Event event) {
String distributionType = (String) event.getProperty(
DistributionEventProperties.DISTRIBUTION_TYPE
);
String[] paths = (String[]) event.getProperty(
DistributionEventProperties.DISTRIBUTION_PATHS
);
switch (distributionType) {
case "ADD":
LOG.info("Content activated: {}", String.join(",", paths));
break;
case "DELETE":
LOG.info("Content deleted: {}", String.join(",", paths));
break;
case "INVALIDATE":
LOG.info("Cache invalidated: {}", String.join(",", paths));
break;
case "TEST":
LOG.debug("Distribution test executed");
break;
default:
LOG.warn("Unknown distribution type: {}", distributionType);
}
}
Only listen to events you need:
// Good: Listen to specific events
property = {
org.osgi.service.event.EventConstants.EVENT_TOPIC + "=" +
DistributionEventTopics.AGENT_PACKAGE_DROPPED
}
// Avoid: Listening to all events if not needed
Event handlers should be fast. Offload heavy processing:
@Reference
private JobManager jobManager;
@Override
public void handleEvent(Event event) {
// Queue a job for async processing
Map<String, Object> jobProperties = new HashMap<>();
jobProperties.put("packageId", event.getProperty(
DistributionEventProperties.DISTRIBUTION_PACKAGE_ID));
jobManager.addJob("com/myapp/distribution/process", jobProperties);
}
Event handlers should use service users, not admin sessions:
<!-- Service user mapping -->
{
"user.mapping": [
"com.myapp.core:distributionEventHandler=myapp-distribution-service"
]
}
Not all properties are available in all events:
String[] paths = (String[]) event.getProperty(
DistributionEventProperties.DISTRIBUTION_PATHS
);
if (paths != null && paths.length > 0) {
// Process paths
}
Use appropriate log levels:
// INFO for normal flow
LOG.info("Package distributed: {}", packageId);
// WARN for unexpected situations
LOG.warn("Package queued longer than expected: {}", packageId);
// ERROR for failures
LOG.error("Package dropped: {}", packageId);
While you can't directly query distribution queues via the API in Cloud Service, you can monitor via:
AGENT_PACKAGE_QUEUED and AGENT_PACKAGE_DISTRIBUTED events/system/console/slingjobsimport java.util.concurrent.ConcurrentHashMap;
import java.util.Map;
@Component(
service = {EventHandler.class, QueueMonitor.class},
property = {
org.osgi.service.event.EventConstants.EVENT_TOPIC + "=" +
DistributionEventTopics.AGENT_PACKAGE_QUEUED,
org.osgi.service.event.EventConstants.EVENT_TOPIC + "=" +
DistributionEventTopics.AGENT_PACKAGE_DISTRIBUTED,
org.osgi.service.event.EventConstants.EVENT_TOPIC + "=" +
DistributionEventTopics.AGENT_PACKAGE_DROPPED
}
)
public class QueueMonitor implements EventHandler {
private final Map<String, Long> queuedPackages = new ConcurrentHashMap<>();
@Override
public void handleEvent(Event event) {
String topic = event.getTopic();
String packageId = (String) event.getProperty(
DistributionEventProperties.DISTRIBUTION_PACKAGE_ID
);
if (DistributionEventTopics.AGENT_PACKAGE_QUEUED.equals(topic)) {
queuedPackages.put(packageId, System.currentTimeMillis());
LOG.info("Queue depth: {}", queuedPackages.size());
// Alert if queue is too deep
if (queuedPackages.size() > 50) {
LOG.warn("Distribution queue depth exceeds threshold: {}",
queuedPackages.size());
}
}
else if (DistributionEventTopics.AGENT_PACKAGE_DISTRIBUTED.equals(topic) ||
DistributionEventTopics.AGENT_PACKAGE_DROPPED.equals(topic)) {
Long queuedTime = queuedPackages.remove(packageId);
if (queuedTime != null) {
long duration = System.currentTimeMillis() - queuedTime;
LOG.info("Package processed in {}ms. Queue depth: {}",
duration, queuedPackages.size());
}
}
}
public int getQueueDepth() {
return queuedPackages.size();
}
}
Causes:
Solution:
// Verify event topic matches exactly
property = {
org.osgi.service.event.EventConstants.EVENT_TOPIC + "=" +
DistributionEventTopics.AGENT_PACKAGE_CREATED // Exact constant
}
// Check component is active in Felix console
// /system/console/components
Cause: Not all properties are available in all events
Solution: Always null-check:
String[] paths = (String[]) event.getProperty(
DistributionEventProperties.DISTRIBUTION_PATHS
);
if (paths == null) {
LOG.warn("No paths in distribution event");
return;
}
Cause: Synchronous processing in event handler
Solution: Use async job processing:
@Reference
private JobManager jobManager;
@Override
public void handleEvent(Event event) {
// Don't do heavy work here
jobManager.addJob("com/myapp/process", eventData);
}
Key Understanding:
com.day.cq.replication.Replicator) - What you call to publish contentorg.apache.sling.distribution.event) - What fires during the distribution lifecycleWorkflow:
Your Code: replicator.replicate(...)
↓
Sling Distribution: Creates package → [AGENT_PACKAGE_CREATED]
↓
Sling Distribution: Queues package → [AGENT_PACKAGE_QUEUED]
↓
Sling Distribution: Sends package → [AGENT_PACKAGE_DISTRIBUTED]
↓
Target Tier: Imports package → [IMPORTER_PACKAGE_IMPORTED]
tools
Use the run-workflow MCP to discover, compose, execute, publish, and save Adobe Firefly workflows. TRIGGER when: user asks what actions are available, what the MCP can do, how to process images/video/3D via workflow, wants to build/run/save/publish a workflow, OR pastes any workflow/batch/execution ID. BARE ID (UUID/workflowId/batchId) = INSPECT ONLY — call inspect_run, NEVER run_workflow_submit. ALWAYS call list_actions first for capability/discovery questions. DO NOT TRIGGER for direct Firefly API calls without MCP (use firefly-api-specs).
tools
Run predefined featured workflows via run-workflow MCP. TRIGGER when user names a featured workflow (retargeting, banners at scale, localization, packaging, banner advertising, etc.) or asks to run a known marketing/production workflow. Requires run-workflow MCP. ALWAYS call get_featured_workflow before compose_workflow. DO NOT TRIGGER for custom one-off workflows with no named template — use run-workflow skill.
tools
Migrate an Adobe Commerce App Builder project from the Integration Starter Kit or Checkout Starter Kit to the new App Management approach. Run from the root of the App Builder project to be migrated. Pass --auto to skip confirmation prompts (suitable for CI or batch use) — auto mode prints a summary of all Q&A questions answered with their defaults. Pass --doc-scan-only to scan README.md and env.dist for outdated content without modifying any files. Use when the user wants to migrate an App Builder project from the Integration Starter Kit or Checkout Starter Kit to the App Management approach, or mentions upgrading their Adobe Commerce extension architecture.
development
Add or modify webhook interceptors in an Adobe Commerce app. Use when the user wants to intercept Commerce operations to validate input, append data, or modify behavior — before or after execution. Requires a base app initialized with commerce-app-init.