| name | replication |
| description | AEM Cloud Service expert skill for replication / content distribution. Covers migration from CQ Replicator (com.day.cq.replication.Replicator) and Sling Replication Agent (org.apache.sling.replication.agent.api) to the Sling Distribution API (Distributor + SimpleDistributionRequest). Includes agent selection (publish vs preview), async response handling, author cluster coordination, service-user setup, review checklist, troubleshooting, and common pitfalls. |
| license | Apache-2.0 |
Replication / Content Distribution — AEM as a Cloud Service
Overview
On AEM as a Cloud Service, replication is performed via the Sling Distribution API (org.apache.sling.distribution.Distributor). The legacy CQ Replicator (com.day.cq.replication.*) and Sling Replication Agent (org.apache.sling.replication.agent.*) APIs are not supported — code using them either compiles against legacy AEM 6.x jars or fails at runtime on CS.
AEMaaCS provides two predefined replication agents:
| Agent name | Targets | Default state |
|---|
publish | Live publish tier | Available by default in every AEMaaCS environment |
preview | Preview tier | Opt-in — only available when the preview tier is enabled for the environment |
publish is the default agent for activation; preview must be explicitly enabled. If both tiers are in use, call distributor.distribute(...) twice — once per agent name. Legacy Replicator.replicate(...) implicitly fanned out to every configured agent; Distributor.distribute(...) is explicit and targets one named agent per call.
Three CS-specific constraints every distribution call must satisfy:
| Constraint | Why |
|---|
Use Distributor + SimpleDistributionRequest — not Replicator or ReplicationAgent | Legacy APIs are removed from the CS SDK |
Resolver via getServiceResourceResolver(SUBSERVICE) — never admin auth or USER/PASSWORD maps | Admin resolvers are unavailable on CS; service-user auth is the only supported path |
Inspect DistributionResponse.isSuccessful() and getState() | Distributor.distribute() returns a queued/accepted response — it does NOT block for delivery |
Distributor.distribute() is asynchronous. A successful response means the distribution request was queued, not delivered. Do not assume content has reached the publish tier just because the call returned isSuccessful() == true. See Expert Guidance below.
Classification — choose before making any changes
Identify the source pattern in the file:
Uses com.day.cq.replication.Replicator with ReplicationAction and ReplicationActionType (ACTIVATE, DEACTIVATE)
→ Apply P1–P4.
Uses org.apache.sling.replication.agent.api.ReplicationAgent with ReplicationResult and agent.replicate(resolver, type, path)
→ Apply P1–P4.
Uses Distributor + SimpleDistributionRequest already
→ Already on the target API — verify against the Review Checklist only.
Uses WorkflowSession.startWorkflow(...) with a replication launcher
→ This skill covers programmatic distribution. If replication is tied to a content workflow step, the workflow handles it — leave the workflow alone, do not introduce a parallel Distributor call.
One pattern per session. If the bundle has multiple legacy classes, migrate one class at a time.
Before starting: Read ../references/aem-cloud-service-pattern-prerequisites.md and apply SCR→DS, service-user, and SLF4J fixes if present in the same changeset.
Discovery
Detection is performed by the analyzer (../scripts/analyze.sh), run by the runbook:
bash ../scripts/analyze.sh <workspace-root> --pattern replication
Match criteria (what the detector flags): a file that imports com.day.cq.replication.Replicator or any org.apache.sling.replication.* type — the legacy replication APIs removed on Cloud Service. One finding per file, at the file's primary type, with the class header as the snippet. Parse-level only — import-based, no type resolution. The modern org.apache.sling.distribution.* API is not flagged.
Analyzer-only: replication has no BPA subtype, so a replication finding originates from the local analyzer, never from a BPA/CAM report.
Resolution contract
guided — apply (guided). The analyzer locates each legacy replication caller; remediation is judgment-based (CQ Replicator / Sling Replication Agent → Sling Distribution API) and applied via P1–P4 in an apply session.
| Site shape | Disposition |
|---|
Replicator / Sling Replication Agent usage in custom code | apply (guided) → P1–P4 |
| Replication tied to a workflow step (workflow owns it) | skipped: workflow-owned |
Test code (src/test/) | skipped: test-scope |
Complete example — before and after
Before (legacy CQ Replicator with admin resolver)
package com.example.replication;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.Service;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;
import com.day.cq.replication.ReplicationAction;
import com.day.cq.replication.ReplicationActionType;
import com.day.cq.replication.Replicator;
import java.util.HashMap;
import java.util.Map;
@Component(immediate = true)
@Service
public class ContentReplicationService {
@Reference
private Replicator replicator;
@Reference
private ResourceResolverFactory resolverFactory;
public void replicateContent(String contentPath) {
ResourceResolver resolver = null;
try {
Map<String, Object> authInfo = new HashMap<>();
authInfo.put(ResourceResolverFactory.USER, "replication-service");
authInfo.put(ResourceResolverFactory.PASSWORD, "password");
resolver = resolverFactory.getAdministrativeResourceResolver(authInfo);
ReplicationAction action = new ReplicationAction(ReplicationActionType.ACTIVATE, contentPath);
replicator.replicate(resolver, action);
System.out.println("Replicated: " + contentPath);
} catch (Exception e) {
System.err.println("Replication failed: " + e.getMessage());
e.printStackTrace();
} finally {
if (resolver != null && resolver.isLive()) {
resolver.close();
}
}
}
}
After — Cloud Service compatible
package com.example.replication;
import org.apache.sling.api.resource.LoginException;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.apache.sling.distribution.DistributionException;
import org.apache.sling.distribution.DistributionRequest;
import org.apache.sling.distribution.DistributionRequestType;
import org.apache.sling.distribution.DistributionResponse;
import org.apache.sling.distribution.Distributor;
import org.apache.sling.distribution.SimpleDistributionRequest;
import org.apache.sling.settings.SlingSettingsService;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collections;
@Component(service = ContentReplicationService.class)
public class ContentReplicationService {
private static final Logger LOG = LoggerFactory.getLogger(ContentReplicationService.class);
private static final String SUBSERVICE = "content-distribution-service";
@Reference
private Distributor distributor;
@Reference
private ResourceResolverFactory resolverFactory;
@Reference
private SlingSettingsService slingSettings;
public void replicateContent(String contentPath) {
if (!slingSettings.getRunModes().contains("author")) {
LOG.debug("Skipping distribution on non-author instance for path: {}", contentPath);
return;
}
try (ResourceResolver resolver = resolverFactory.getServiceResourceResolver(
Collections.singletonMap(ResourceResolverFactory.SUBSERVICE, SUBSERVICE))) {
DistributionRequest request = new SimpleDistributionRequest(
DistributionRequestType.ADD, false, contentPath);
DistributionResponse response = distributor.distribute("publish", resolver, request);
if (response.isSuccessful()) {
LOG.info("Content distribution queued for path: {} (state={})",
contentPath, response.getState());
} else {
LOG.warn("Content distribution not queued for path: {} (state={}, message={})",
contentPath, response.getState(), response.getMessage());
}
} catch (LoginException e) {
LOG.error("Could not open service resolver for subservice '{}'", SUBSERVICE, e);
} catch (DistributionException e) {
LOG.error("Distribution failed for path: {}", contentPath, e);
}
}
}
Required Repoinit (goes in your ui.config Repoinit OSGi config):
create service user content-distribution-service
set ACL for content-distribution-service
allow jcr:read,crx:replicate on /content
end
The crx:replicate privilege is required for the service user to initiate distribution — read access alone is not sufficient.
Required service-user mapping (ui.config, file named e.g. org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended-content-distribution.cfg.json):
{
"user.mapping": [
"com.example.mybundle:content-distribution-service=[content-distribution-service]"
]
}
P1 — Replace Replicator / ReplicationAgent with Distributor
For com.day.cq.replication.Replicator:
@Reference
private Replicator replicator;
ReplicationAction action = new ReplicationAction(ReplicationActionType.ACTIVATE, path);
replicator.replicate(resolver, action);
@Reference
private Distributor distributor;
DistributionRequest request = new SimpleDistributionRequest(
DistributionRequestType.ADD, false, path);
DistributionResponse response = distributor.distribute("publish", resolver, request);
For org.apache.sling.replication.agent.api.ReplicationAgent:
@Reference
private ReplicationAgent agent;
ReplicationResult result = agent.replicate(resolver, ReplicationActionType.ADD, path);
if (result.isSuccessful()) { }
@Reference
private Distributor distributor;
DistributionRequest request = new SimpleDistributionRequest(
DistributionRequestType.ADD, false, path);
DistributionResponse response = distributor.distribute("publish", resolver, request);
if (response.isSuccessful()) { }
ReplicationActionType → DistributionRequestType mapping:
Legacy ReplicationActionType | DistributionRequestType |
|---|
ACTIVATE | ADD |
DEACTIVATE | DELETE |
ADD | ADD |
DELETE | DELETE |
INTERNAL_POLL | No equivalent — drop (CS does not expose poll-style replication) |
TEST | No equivalent — drop |
SimpleDistributionRequest constructor:
new SimpleDistributionRequest(
DistributionRequestType type,
boolean deep,
String... paths
)
deep = true distributes the path and all descendants. Use sparingly — a single deep=true on /content/mysite can queue thousands of nodes.
- Pass multiple paths for a single batched request:
new SimpleDistributionRequest(ADD, false, path1, path2, path3).
Distributing to both publish and preview tiers:
DistributionResponse publishResponse = distributor.distribute("publish", resolver, request);
DistributionResponse previewResponse = distributor.distribute("preview", resolver, request);
Two separate calls — there is no "distribute to all" agent name.
Handle the "preview not enabled" case gracefully. Because preview is opt-in per environment, code that distributes to both tiers must tolerate the absence of the preview agent. The DistributionResponse for an unconfigured agent comes back as DROPPED with a "no matching agent" message — this is not an error condition in environments that haven't enabled preview:
DistributionResponse previewResponse = distributor.distribute("preview", resolver, request);
if (previewResponse.isSuccessful()) {
LOG.info("Preview distribution queued for path: {}", path);
} else if (previewResponse.getMessage() != null
&& previewResponse.getMessage().contains("no matching agent")) {
LOG.debug("Preview agent not configured; skipping preview distribution for {}", path);
} else {
LOG.warn("Preview distribution failed for {}: state={}, message={}",
path, previewResponse.getState(), previewResponse.getMessage());
}
Treating "no matching agent" as a warning rather than an info-level log in a publish-only environment creates alert noise; treating it as an error breaks deployments. Inspect the message and log at debug.
P2 — Update imports
Remove (CQ Replicator):
import com.day.cq.replication.ReplicationAction;
import com.day.cq.replication.ReplicationActionType;
import com.day.cq.replication.Replicator;
import com.day.cq.replication.ReplicationException;
Remove (Sling Replication Agent):
import org.apache.sling.replication.agent.api.ReplicationAgent;
import org.apache.sling.replication.agent.api.ReplicationAgentConfiguration;
import org.apache.sling.replication.agent.api.ReplicationAgentException;
import org.apache.sling.replication.agent.api.ReplicationResult;
import org.apache.sling.replication.agent.api.ReplicationActionType;
import org.apache.sling.replication.agent.impl.SimpleReplicationAgent;
Remove (SCR → DS):
import org.apache.felix.scr.annotations.*;
Add:
import org.apache.sling.api.resource.LoginException;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.apache.sling.distribution.DistributionException;
import org.apache.sling.distribution.DistributionRequest;
import org.apache.sling.distribution.DistributionRequestType;
import org.apache.sling.distribution.DistributionResponse;
import org.apache.sling.distribution.Distributor;
import org.apache.sling.distribution.SimpleDistributionRequest;
import org.apache.sling.settings.SlingSettingsService;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collections;
P3 — Service-user resolver and auth
Remove all legacy auth patterns:
resolver = resolverFactory.getAdministrativeResourceResolver(null);
Map<String, Object> authInfo = new HashMap<>();
authInfo.put(ResourceResolverFactory.USER, "...");
authInfo.put(ResourceResolverFactory.PASSWORD, "...");
resolver = resolverFactory.getResourceResolver(authInfo);
Replace with a service-user resolver in try-with-resources:
try (ResourceResolver resolver = resolverFactory.getServiceResourceResolver(
Collections.singletonMap(ResourceResolverFactory.SUBSERVICE, "content-distribution-service"))) {
} catch (LoginException e) {
LOG.error("Could not open service resolver", e);
}
getServiceResourceResolver throws LoginException on failure — it does not normally return null. Catch LoginException; do not add a resolver == null branch unless a custom wrapper is in use.
Service user privileges: the user needs crx:replicate (in addition to jcr:read) on every path it distributes. Without it, Distributor.distribute(...) returns a non-successful response with a "not authorized" message. Permissions must be declared directly on the service user — do not rely on permissions inherited via group membership.
Mapping file conventions:
- Use the amend form (
ServiceUserMapperImpl.amended-<name>.cfg.json) — do not edit the base ServiceUserMapperImpl config.
- Use principal-name mapping: the square-bracket form
=[service-user-name] (as shown above) is the principal-name form.
- Do not use the deprecated
userName form (single user without brackets) — it is being phased out across Sling.
P4 — Inspect DistributionResponse
Distributor.distribute(...) returns a DistributionResponse describing the outcome of queuing, not the outcome of delivery. Always inspect it:
DistributionResponse response = distributor.distribute("publish", resolver, request);
if (response.isSuccessful()) {
LOG.info("Distribution queued: state={}", response.getState());
} else {
LOG.warn("Distribution not queued: state={}, message={}",
response.getState(), response.getMessage());
}
DistributionResponse.getState() values (most common):
| State | Meaning |
|---|
ACCEPTED | Request queued for processing |
DISTRIBUTED | Request delivered (returned by some agents/configurations) |
DROPPED | Request rejected — typically a permission, validation, or queue-full issue. Inspect getMessage() |
NOT_EXECUTED | Distribution not attempted |
Do not poll for DISTRIBUTED. Distribution is asynchronous; the response is your only synchronous signal. For delivery visibility, use the Distribution UI (Tools > Deployment > Distribution) or operational monitoring — see Modern Alternatives and Practical engineering notes.
Greenfield — writing a new distribution caller from scratch
When writing new distribution code (not migrating legacy), use this template. It bundles the AEMaaCS best practices into one self-contained service: run-mode guard, service-user resolver in try-with-resources, agent iteration with defensive preview handling, and JobResult-style outcome classification.
package com.example.distribution;
import org.apache.sling.api.resource.LoginException;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.apache.sling.distribution.DistributionException;
import org.apache.sling.distribution.DistributionRequest;
import org.apache.sling.distribution.DistributionRequestType;
import org.apache.sling.distribution.DistributionResponse;
import org.apache.sling.distribution.Distributor;
import org.apache.sling.distribution.SimpleDistributionRequest;
import org.apache.sling.settings.SlingSettingsService;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
@Component(service = MyDistributionService.class)
public class MyDistributionService {
private static final Logger LOG = LoggerFactory.getLogger(MyDistributionService.class);
private static final String SUBSERVICE = "my-distribution-service";
private static final List<String> AGENTS = Arrays.asList("publish", "preview");
@Reference
private Distributor distributor;
@Reference
private ResourceResolverFactory resolverFactory;
@Reference
private SlingSettingsService slingSettings;
public void distribute(String path, DistributionRequestType type) {
if (!slingSettings.getRunModes().contains("author")) {
LOG.debug("Skipping distribution on non-author instance for path: {}", path);
return;
}
try (ResourceResolver resolver = resolverFactory.getServiceResourceResolver(
Collections.singletonMap(ResourceResolverFactory.SUBSERVICE, SUBSERVICE))) {
DistributionRequest request = new SimpleDistributionRequest(type, false, path);
for (String agent : AGENTS) {
distributeToAgent(agent, resolver, request, path);
}
} catch (LoginException e) {
LOG.error("Could not open service resolver for '{}'", SUBSERVICE, e);
}
}
private void distributeToAgent(String agent, ResourceResolver resolver,
DistributionRequest request, String path) {
try {
DistributionResponse response = distributor.distribute(agent, resolver, request);
if (response.isSuccessful()) {
LOG.info("[{}] distribution queued for {} (state={})",
agent, path, response.getState());
} else if (isAgentNotConfigured(response)) {
LOG.debug("[{}] agent not configured; skipping", agent);
} else {
LOG.warn("[{}] distribution failed for {}: state={}, message={}",
agent, path, response.getState(), response.getMessage());
}
} catch (DistributionException e) {
LOG.error("[{}] distribution exception for {}", agent, path, e);
}
}
private static boolean isAgentNotConfigured(DistributionResponse response) {
String message = response.getMessage();
return message != null && message.contains("no matching agent");
}
}
What this template captures:
- Single resolver per call — opened in try-with-resources, reused for all agents in one invocation
- Per-agent iteration — adding a new agent in future means editing one list, not new code paths
- Defensive preview handling — opt-in agent absence is logged at
debug, not warn
- Distinct logging for the three outcomes — queued (
info), agent-not-configured (debug), real failure (warn)
DistributionException caught per agent — one transport error doesn't abort the other agents
Composition — calling Distributor from a JobConsumer
A common production pattern is ResourceChangeListener → JobConsumer → Distributor: the listener stays lightweight (per the resource-change-listener skill), the consumer runs the distribution off the listener thread and gets Sling Job retry semantics for free.
import org.apache.sling.api.resource.LoginException;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.apache.sling.distribution.DistributionException;
import org.apache.sling.distribution.DistributionRequest;
import org.apache.sling.distribution.DistributionRequestType;
import org.apache.sling.distribution.DistributionResponse;
import org.apache.sling.distribution.Distributor;
import org.apache.sling.distribution.SimpleDistributionRequest;
import org.apache.sling.event.jobs.Job;
import org.apache.sling.event.jobs.consumer.JobConsumer;
import org.apache.sling.event.jobs.consumer.JobResult;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collections;
@Component(
service = JobConsumer.class,
property = { JobConsumer.PROPERTY_TOPICS + "=com/example/replicate" }
)
public class ReplicationJobConsumer implements JobConsumer {
public static final String JOB_TOPIC = "com/example/replicate";
private static final Logger LOG = LoggerFactory.getLogger(ReplicationJobConsumer.class);
private static final String SUBSERVICE = "content-distribution-service";
@Reference
private Distributor distributor;
@Reference
private ResourceResolverFactory resolverFactory;
@Override
public JobResult process(final Job job) {
final String path = job.getProperty("path", String.class);
if (path == null) {
LOG.warn("Job missing 'path' property; CANCEL");
return JobResult.CANCEL;
}
try (ResourceResolver resolver = resolverFactory.getServiceResourceResolver(
Collections.singletonMap(ResourceResolverFactory.SUBSERVICE, SUBSERVICE))) {
DistributionRequest request = new SimpleDistributionRequest(
DistributionRequestType.ADD, false, path);
DistributionResponse response = distributor.distribute("publish", resolver, request);
if (response.isSuccessful()) {
return JobResult.OK;
}
String message = response.getMessage();
if (message != null
&& (message.contains("not authorized") || message.contains("no matching agent"))) {
LOG.error("Unrecoverable distribution failure for {}: {}", path, message);
return JobResult.CANCEL;
}
LOG.warn("Transient distribution failure for {}: {}", path, message);
return JobResult.FAILED;
} catch (LoginException e) {
LOG.error("Could not open service resolver for '{}'", SUBSERVICE, e);
return JobResult.FAILED;
} catch (DistributionException e) {
LOG.error("Distribution exception for {}", path, e);
return JobResult.FAILED;
}
}
}
Key composition rules:
- Run-mode guard belongs on the trigger (listener), not the consumer. Sling Jobs may run on any author pod; the listener decides whether to enqueue in the first place.
- Map
DistributionResponse → JobResult deliberately. Permission/agent errors are unrecoverable (CANCEL); transient queue/transport errors are retryable (FAILED).
- Share the topic constant. The listener and consumer both reference
JOB_TOPIC (or a shared constants class) — never copy the literal string into both.
- Service user must own
crx:replicate on the paths the consumer distributes, regardless of which pod runs the job.
Author-only execution and cluster coordination
Distribution is initiated from author. Running it on publish has no effect (publish has no distribution agents pointing outward) and indicates a code-path bug.
Guard at the top of the public method:
@Reference
private SlingSettingsService slingSettings;
public void replicate(String path) {
if (!slingSettings.getRunModes().contains("author")) {
return;
}
}
Author-only does not mean single execution. In AEMaaCS author clusters, multiple author pods may receive the same trigger (e.g. a ResourceChangeListener event delivered to each node). If the operation must distribute exactly once per logical change, use a cluster-coordination pattern — leader-instance check (SlingSettingsService topology APIs), distributed lock, or idempotent design at the trigger layer. A run-mode check alone guarantees neither uniqueness nor ordering across the author cluster.
Review Checklist
Cross-cutting:
Distribution call:
Service user / config:
Execution context:
Troubleshooting
| Symptom | Log to search | Fix direction |
|---|
LoginException: Unable to retrieve the service resource resolver | the exception name verbatim | Service user not provisioned via Repoinit, OR ServiceUserMapperImpl.amended-*.cfg.json missing/misnamed, OR bundle symbolic name in the mapping doesn't match the bundle owning the distribution caller |
DistributionResponse.getState() == DROPPED with "not authorized" message | state=DROPPED and not authorized in getMessage() | Service user lacks crx:replicate — extend Repoinit ACL to add crx:replicate on the distributed paths |
DistributionResponse.getState() == DROPPED with "no matching agent" | agent not found or no matching agent | First argument to distribute(...) is a legacy/custom agent name not present on AEMaaCS — use "publish" or "preview" |
| Distribution call returns successful but content never appears on publish | none synchronously — in the Distribution UI (Tools > Deployment > Distribution), open the specific agent (publish or preview), then check Queue items for per-item state and Logs for per-item delivery errors | Distribution is asynchronous; the response means queued, not delivered. In the Distribution UI, items move from a persisted state to fully published. Items stuck in persisted indicate the publish tier hasn't drained the queue — open the item to see the failure reason. If the agent's overall status shows blocked or paused, distribution is halted for that agent regardless of individual item state |
| Same path queued N times in quick succession | repeating Distribution queued for path: … entries | Trigger fires on every author cluster pod — add idempotency check or leader-only guard. The log noise indicates a real coordination bug even if the final delivered outcome looks correct |
ClassNotFoundException: com.day.cq.replication.Replicator at runtime | the exception name verbatim | Legacy Replicator API is not on the CS SDK classpath — code wasn't fully migrated; complete the P1 + P2 steps |
ClassNotFoundException: org.apache.sling.replication.agent.* at runtime | the exception name verbatim | Same as above, for the Sling Replication Agent API |
Common Pitfalls
Treating isSuccessful() as "delivered" — the response is synchronous; delivery is asynchronous. A successful response means "queued", not "received by publish". Downstream code that assumes immediate availability on publish will see stale or missing content.
Hardcoding a legacy agent name — copying "default", "socialpubsvc", or a customer-specific on-prem agent name into distribute("...", ...). On AEMaaCS the only generally-available agents are "publish" and "preview".
Distributing from publish — code paths that invoke Distributor.distribute(...) from publish will silently no-op or fail (no outbound agents). Always guard with SlingSettingsService.getRunModes().contains("author").
Using getAdministrativeResourceResolver() or USER/PASSWORD auth maps — both are unsupported on CS. The only resolver path is getServiceResourceResolver(SUBSERVICE) with a Repoinit-provisioned service user.
Missing crx:replicate on the service user — without this privilege, Distributor.distribute(...) returns DROPPED. jcr:read alone is not enough.
deep=true on broad paths — new SimpleDistributionRequest(ADD, true, "/content") distributes every descendant. Often unintended and can flood the distribution queue. Use deep=true only when you genuinely need recursive distribution.
Building a custom bulk loop with distribute() — Adobe recommends the Tree Activation workflow step for bulk publishing rather than code that iterates pages calling distribute() per path. The workflow handles batching, throttling, and queue back-pressure. Reserve programmatic distribute() for narrow, targeted activations.
Forgetting to distribute to preview when both tiers are in use — if the site has both publish and preview tiers, distributing only to "publish" leaves preview stale.
Polling for completion — looping on Distributor.distribute(...) calls or sleeping to "wait for replication" is a code smell. Distribution is fire-and-forget from the caller's perspective; use the Distribution UI (Tools > Deployment > Distribution) or operational monitoring for delivery visibility.
Catching Exception and swallowing it — DistributionException carries the actual failure cause (permission, queue config, transport). Catching the broad Exception and logging only the message loses the stack trace and root cause.
Modern Alternatives
| Need | Use |
|---|
| Programmatic single-path activation from code | Distributor.distribute("publish", resolver, request) (this skill) |
| Recursive activation of a content tree | SimpleDistributionRequest(ADD, true, path) — used carefully |
| Multiple paths in one batch | SimpleDistributionRequest(ADD, false, path1, path2, ...) |
| Activation as part of an editorial workflow | AEM Workflow + the built-in Activate Page / Deactivate Page workflow step — do not add a parallel Distributor call |
| Bulk activation of a tree (multiple pages/assets at once) | AEM Tree Activation workflow step — Adobe's recommended approach for bulk publishing. Do not build a custom loop calling distribute() per path |
| Schedule-based distribution | Sling Distribution Trigger (configured via OSGi factory configs, not code) |
| Notify external systems when content changes | AEM Eventing (not replication) — replication is for moving content between AEM tiers, not for external notification |
| Initial environment seeding / large-scale content sync | AEM Content Transfer Tool (Cloud Manager) — not a code path |
| Asset distribution to delivery tiers | Distribution agent named "publish" handles assets the same way as pages |
Expert Guidance
Distribution is asynchronous; design for it. The DistributionResponse tells you whether the request was accepted into the queue, not whether content has reached publish. Treat distribution like sending an email: the API confirms it left your outbox, not that the recipient read it. Use the Distribution UI (Tools > Deployment > Distribution) and the queue state model (persisted → fully published) for delivery visibility — not per-call assertions in code.
Failure modes to plan for:
- Permission denied —
DROPPED state with "not authorized"; fix the service-user ACL (crx:replicate)
- Agent not found —
DROPPED with "no matching agent"; fix the agent name (publish / preview)
- Queue full / back-pressure —
DROPPED or NOT_EXECUTED; transient, retry with backoff
DistributionException thrown — the request didn't reach the queue; check the exception cause and resolver state
Do not invent agent names. AEMaaCS provides publish and preview as the predefined agents (publish available by default, preview opt-in per environment). Custom agent names from on-prem setups ("socialpubsvc", "flush", "reverse", customer-specific names) do not exist on CS. If a customer's legacy code references such names, the migration is to map them to publish / preview based on intent, not to recreate the agent.
Preview tier is opt-in. Even teams that "only have publish" today should be aware: if the site introduces preview later, every distribute("publish", ...) call site becomes a place that needs a second distribute("preview", ...) call. Wrapping the distribution in a small helper that takes the agent list as a parameter is cheap insurance.
Practical engineering notes
The following are practical observations from production AEMaaCS code rather than items documented as part of Adobe's official Distribution API contract. They are useful design guidance but should not be treated as guarantees:
- Author-cluster duplicate calls are usually harmless. When triggers fire on multiple author cluster pods, the underlying distribution agent typically de-duplicates identical pending requests for the same path. Don't rely on this — design idempotency in your trigger logic — but it explains why duplicate
distribute() calls in logs often produce a single delivered outcome.
- Distribution events on author for observability. Sling Distribution publishes OSGi events on topics like
org/apache/sling/distribution/agent/package/distributed. Subscribing to these from an EventHandler or JobConsumer is a workable pattern for code that needs to react after a successful distribution, but the topic names and payload are Sling implementation detail, not part of the documented AEMaaCS contract. Verify against the Sling version shipped with your SDK before relying on them.
- Don't poll the publish tier from author to check whether content arrived. Use the Distribution UI or operational monitoring instead.