# Replication

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

- **Type:** Skill
- **Install:** `agentstack add skill-adobe-skills-replication`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [adobe](https://agentstack.voostack.com/s/adobe)
- **Installs:** 0
- **Category:** [Agent Skills](https://agentstack.voostack.com/c/agent-skills)
- **Latest version:** 0.1.0
- **License:** Apache-2.0
- **Upstream author:** [adobe](https://github.com/adobe)
- **Source:** https://github.com/adobe/skills/tree/main/plugins/aem/cloud-service/skills/code-assessment/replication
- **Website:** https://www.adobe.com/ai/overview.html

## Install

```sh
agentstack add skill-adobe-skills-replication
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# 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](#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](#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`](../references/aem-cloud-service-pattern-prerequisites.md) and apply SCR→DS, service-user, and SLF4J fixes if present in the same changeset.

---

## Complete example — before and after

### Before (legacy CQ Replicator with admin resolver)

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

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

```json
{
  "user.mapping": [
    "com.example.mybundle:content-distribution-service=[content-distribution-service]"
  ]
}
```

---

## P1 — Replace Replicator / ReplicationAgent with Distributor

**For `com.day.cq.replication.Replicator`:**

```java
// BEFORE
@Reference
private Replicator replicator;

ReplicationAction action = new ReplicationAction(ReplicationActionType.ACTIVATE, path);
replicator.replicate(resolver, action);

// AFTER
@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`:**

```java
// BEFORE
@Reference
private ReplicationAgent agent;

ReplicationResult result = agent.replicate(resolver, ReplicationActionType.ADD, path);
if (result.isSuccessful()) { /* ... */ }

// AFTER
@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:**

```java
new SimpleDistributionRequest(
    DistributionRequestType type,  // ADD or DELETE
    boolean deep,                  // include descendants (recursive distribution)
    String... paths                // one or more 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:**

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

```java
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")) {
    // Preview tier not enabled for this environment — expected on publish-only setups
    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):**
```java
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):**
```java
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):**
```java
import org.apache.felix.scr.annotations.*;
```

**Add:**
```java
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;            // only if guarding by run mode
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:

```java
// REMOVE — admin auth is unavailable on CS
resolver = resolverFactory.getAdministrativeResourceResolver(null);

// REMOVE — USER/PASSWORD auth maps do not work on CS
Map 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:

```java
try (ResourceResolver resolver = resolverFactory.getServiceResourceResolver(
        Collections.singletonMap(ResourceResolverFactory.SUBSERVICE, "content-distribution-service"))) {
    // distribution call
} 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-.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:

```java
DistributionResponse response = distributor.distribute("publish", resolver, request);

if (response.isSuccessful()) {
    LOG.info("Distribution queued: state={}", response.getState());
} else {
    // common non-successful states: DROPPED, NOT_EXECUTED
    LOG.warn("Distribution not queued: state={}, message={}",
             response.getState(), response.getMessage());
}
```

`DistributionResponse.getState()` values (most common):

| State | Meaning |
|-------|---------|
| `ACCEPTED` | Request queued f

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [adobe](https://github.com/adobe)
- **Source:** [adobe/skills](https://github.com/adobe/skills)
- **License:** Apache-2.0
- **Homepage:** https://www.adobe.com/ai/overview.html

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-adobe-skills-replication
- Seller: https://agentstack.voostack.com/s/adobe
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
