Home Services Blog → Adobe Analytics → Adobe Experience Platform → Adobe Target → AEM → Google Analytics 4 → Segment CDP About Contact
AEM

Why jcr:createdBy Reflects a System User on AEM Cloud Publish

Your content authors create pages on AEM Author and the jcr:createdBy correctly reflects their identity. But once a page is published, the same property on the Publish tier reflects a system user — the distribution service — instead of the original author. This post explains exactly why this happens, what it means for your headless APIs and audit workflows, and the right engineering pattern to fix it.

AEM Cloud Service Content Distribution Architecture

1. The Problem

Imagine a team building a headless API on top of AEM Cloud Service. They expose page metadata — title, tags, creation date, and creator — through a Sling Model backed by a JSON exporter. On the Author preview everything looks correct: jcr:createdBy shows [email protected].

Then they test the same endpoint on Publish. The creator field reflects a system user — the distribution service — instead of the real author. Support tickets open. The team suspects a bug in their code. Debugging begins.

There is no bug. This is how AEM Cloud Service works — by design.

The following side-by-side illustrates the discrepancy:

✦ Author Instance — CRXDE
jcr:primaryType
cq:PageContent
jcr:createdBy
jcr:lastModifiedBy
cq:lastReplicatedBy
cq:lastReplicated
2026-04-13T10:30Z
⚠ Publish Instance — CRXDE
jcr:primaryType
cq:PageContent
jcr:createdBy
sling-distribution-importer
jcr:lastModifiedBy
sling-distribution-importer
cq:lastReplicatedBy
cq:lastReplicated
2026-04-13T10:30Z

Notice that cq:lastReplicatedBy is intact — it correctly records who triggered the publish action. Only the JCR-managed properties (jcr:createdBy and jcr:lastModifiedBy) differ. Understanding why requires a quick look at the AEM Cloud publishing pipeline.

2. How Publishing Works in AEM Cloud

Legacy AEM 6.5 / AMS — Classic Replication

In AEM 6.5 and Managed Services, content is moved from Author to Publish through replication agents. An agent opens an HTTP connection directly to a specific Publish instance, serialises the content into a package, and POSTs it. The Publish instance unpacks the content using a configured replication-receiver service user. The same root cause applies here — that service user becomes jcr:createdBy on Publish — but the behaviour is less visible because teams rarely inspect CRXDE on Publish.

AEM as a Cloud Service — Sling Content Distribution

AEMaaCS replaces replication agents entirely with Sling Content Distribution (SCD). The architecture is fundamentally different:

AEM Cloud — Content Distribution Pipeline
✍️
Author
jcr:createdBy = real user
→
📦
SCD Queue
Binary package
→
☁️
Adobe Pipeline
Managed by Adobe
→
🌐
Publish Pod(s)
Node created by importer
→
⚡
CDN / Fastly
Auto-invalidated
★ At the Publish Pod, the binary package is applied by the sling-distribution-importer service session — not the original author's session.

Key architectural properties of SCD that matter here:

  • Stateless pipeline: The distribution pipeline is managed entirely by Adobe outside the AEM runtime. There is no persistent HTTP connection between Author and Publish like in classic replication.
  • Publish-pod auto-scaling: Publish pods can be added or removed dynamically. There is no per-instance agent to configure; all pods subscribe to the same distribution queue.
  • Binary package import: Content is serialized into a distribution package (Apache Jackrabbit Vault format) on Author and deserialized on Publish by a dedicated service user. The active session on Publish at import time is sling-distribution-importer.
  • Out-of-runtime execution: The serialization and pipeline handoff happens outside the normal AEM request cycle — there is no author user session in scope on the Publish side.
Push vs Pull

In AEM 6.5, Author pushes to Publish via HTTP. In AEM Cloud, Publish pulls packages from a queue. This inversion is why author session context is fundamentally unavailable on the Publish side during import.

3. Why jcr:createdBy Changes on Publish

JCR Protected Properties — The Specification

The JCR 2.0 specification (JSR-283) defines a category of protected properties. These properties are managed exclusively by the repository implementation and cannot be set, modified, or overridden by application code or imported content. The relevant protected properties in AEM are:

PropertyTypeSet ByWritable by App?
jcr:created Date Repository at node creation No
jcr:createdBy String Repository at node creation (session user) No
jcr:lastModified Date Repository on every save No
jcr:lastModifiedBy String Repository on every save (session user) No
cq:lastReplicatedBy String AEM replication layer (before distribution) Yes
cq:lastReplicated Date AEM replication layer Yes
custom:createdBy String Your application code Custom

What Happens During Package Import

When the distribution package arrives at a Publish pod, the distribution mechanism applies it under a dedicated service session. That session calls Node.addNode() (or equivalent) for each node in the package. Because the node is being created for the first time in the Publish repository, the JCR sets jcr:createdBy from the active session at that moment:

// Set automatically by the repository — cannot be overridden jcr:createdBy = "sling-distribution-importer" // active session user jcr:created = "2026-04-13T10:30:05.000Z" // time of import on Publish // Even if the package contained these values, the repository ignores them. // Protected properties in the incoming package are silently discarded.

The distribution package does contain the original jcr:createdBy value from Author. However, protected properties are explicitly excluded from the import contract. The Apache Jackrabbit Oak importer will not apply them — it generates new values from the active session. This is not a gap in AEM's implementation; it is correct JCR-specification behaviour.

Why the Original User Context is Unavailable

In classic AEM 6.5 replication, there was at least a concept of a "replication user" whose credentials were explicitly configured in the agent. In AEM Cloud, the distribution pipeline is operated by Adobe infrastructure. By the time the package reaches the Publish pod:

  • The original author's browser session has long since closed.
  • There is no delegated user token or impersonation chain that spans Author → Pipeline → Publish.
  • The only session available on Publish during import is the distribution mechanism's service session — there is no way to impersonate the original author across this boundary.
Not Documented Explicitly by Adobe

As of the time of writing, Adobe's public documentation does not call this out explicitly as a named behaviour. It is an emergent consequence of the JCR specification combined with the SCD architecture. Teams typically discover it by inspecting CRXDE on Publish or by debugging headless API responses.

4. Impact on Headless APIs and Reporting

JSON Exporters Returning Incorrect Creator

AEM's built-in Sling Model Exporter (and any custom JSON exporter backed by a ValueMap read on Publish) will return a system user identity if it reads jcr:createdBy directly from Publish. On Publish, content is created by the distribution mechanism, so jcr:createdBy may reflect a system user such as the distribution service — corrupting any front-end that renders a byline, author card, or "written by" attribution.

Audit and Compliance Challenges

Organisations subject to content governance requirements (financial services, healthcare, publishing) often need to trace which human created a piece of content for audit purposes. If audit tooling queries Publish directly and reads jcr:createdBy, audit trails will show only the system user — making it impossible to identify the responsible author without cross-referencing the Author tier.

Personalisation and Ownership Workflows

Teams using jcr:createdBy to route content to an author's queue, build personalised dashboards, or send notifications will find that Publish-side queries return no meaningful results — everything appears owned by the distribution service.

Cross-Tier Reporting

Analytics pipelines or CMS reports that aggregate data from Publish instances will produce misleading creator distributions — thousands of pages attributed to a single system user.

5. Recommended Solution: Custom Authorship Property

The only reliable way to preserve original authorship across the distribution boundary is to store it in a custom, non-protected JCR property on the jcr:content node on Author, before the content is distributed. Because this property is not protected, the distribution package preserves it and the Publish repository applies it as-is.

Choosing the Right Property Name

Use a namespaced property to avoid collisions with AEM's built-in namespace. Common conventions:

  • custom:createdBy — explicit custom namespace
  • app:originalAuthor — application-specific namespace
  • ww:createdBy — vendor-prefixed (used in this guide)

Target JCR Structure

/content/mysite/en/homepage └── jcr:content [cq:PageContent] ├── jcr:title = "Homepage" ├── jcr:createdBy = "sling-distribution-importer" ← set by distribution mechanism on Publish ├── jcr:lastModifiedBy = "sling-distribution-importer" ← set by distribution mechanism on Publish ├── cq:lastReplicatedBy = "[email protected]" ← survives intact ├── ww:createdBy = "[email protected]" ← custom, survives intact ✓ └── ww:lastPublishedBy = "[email protected]" ← custom, survives intact ✓

Immutability — Set Once, Never Overwrite

ww:createdBy should be written once at page creation time and never changed again. It represents the original author. ww:lastPublishedBy can be updated on every publish action.

Migration Strategy for Existing Content

For existing pages that don't yet have ww:createdBy, run a one-time content migration script on Author that backfills from jcr:createdBy (which is accurate on Author):

Groovy — AEM Groovy Console
import com.day.cq.wcm.api.Page
import com.day.cq.wcm.api.PageManager

def pm = resourceResolver.adaptTo(PageManager)
def root = pm.getPage('/content/mysite')

root.listChildren(null, true).each { Page page ->
    def content = page.contentResource
    if (content == null) return

    def props = content.adaptTo(ModifiableValueMap)
    if (props == null) return

    // Only backfill if not already set
    if (!props.containsKey('ww:createdBy')) {
        def original = props.get('jcr:createdBy', String)
        if (original) {
            props.put('ww:createdBy', original)
            println "Backfilled: ${page.path} → ${original}"
        }
    }
}

resourceResolver.commit()
println "Migration complete."
Run on Author Only

Always run the migration script on the Author instance and then re-publish the affected pages. Running it on Publish directly would backfill a system user reflecting the distribution mechanism — exactly the wrong value.

6. Implementation Approaches

Option A — ResourceChangeListener (Preferred for AEM Cloud)

Listen for node-added events under /content on Author. When a jcr:content node is first created, capture the creating user and write the custom property immediately. This fires synchronously in the same request cycle as page creation, so the author's session is readily available.

Java — OSGi Component
package com.acme.core.listeners;

import org.apache.sling.api.resource.*;
import org.apache.sling.spi.resource.provider.ResolveContext;
import org.osgi.service.component.annotations.*;
import org.osgi.service.event.Event;
import org.osgi.service.event.EventHandler;
import org.apache.jackrabbit.api.security.user.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jcr.Session;
import java.util.*;

import static org.apache.sling.api.resource.observation.ResourceChangeListener.*;

@Component(
    service = { EventHandler.class },
    immediate = true,
    property = {
        "event.topics=org/apache/sling/api/resource/Resource/ADDED",
        "event.filter=(path=/content/**/jcr:content)"
    }
)
public class PageAuthorshipListener implements EventHandler {

    private static final Logger log = LoggerFactory.getLogger(PageAuthorshipListener.class);
    private static final String CUSTOM_CREATED_BY = "ww:createdBy";

    @Reference
    private ResourceResolverFactory resolverFactory;

    @Override
    public void handleEvent(Event event) {
        String path = (String) event.getProperty("path");
        if (path == null || !path.endsWith("/jcr:content")) return;

        Map authInfo = Collections.singletonMap(
            ResourceResolverFactory.SUBSERVICE, "authorship-service"
        );

        try (ResourceResolver serviceResolver = resolverFactory.getServiceResourceResolver(authInfo)) {

            Resource jcrContent = serviceResolver.getResource(path);
            if (jcrContent == null) return;

            ModifiableValueMap props = jcrContent.adaptTo(ModifiableValueMap.class);
            if (props == null) return;

            // Write only once — never overwrite the original author
            if (!props.containsKey(CUSTOM_CREATED_BY)) {
                // jcr:createdBy is accurate on Author at this point
                String originalAuthor = props.get("jcr:createdBy", String.class);
                if (originalAuthor != null && !originalAuthor.isBlank()) {
                    props.put(CUSTOM_CREATED_BY, originalAuthor);
                    serviceResolver.commit();
                    log.debug("Set {} = {} on {}", CUSTOM_CREATED_BY, originalAuthor, path);
                }
            }

        } catch (Exception e) {
            log.error("Failed to set authorship property on {}", path, e);
        }
    }
}

Service User Configuration

The listener uses a service user named authorship-service. Register it in your OSGi configuration:

OSGi Config — .cfg.json
{
  "user.mapping": [
    "com.acme.core:authorship-service=[authorship-service]"
  ]
}

Create the system user authorship-service in the AEM User Manager and grant it rep:write on /content. In AEM Cloud, add the principal to your repository-initializer repoinit script:

repoinit
create service user authorship-service

set ACL on /content
    allow jcr:read,rep:write for authorship-service
end

Option B — Workflow Launcher on Page Creation

For teams that prefer workflow-based automation, configure a Workflow Launcher that triggers on NODE_ADDED under /content matching jcr:content nodes, and attach a workflow model with a single custom process step:

Java — Workflow Process Step
package com.acme.core.workflow;

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 org.apache.sling.api.resource.*;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Component(
    service = WorkflowProcess.class,
    property = { "process.label=Set Custom Authorship Property" }
)
public class SetAuthorshipWorkflowProcess implements WorkflowProcess {

    private static final Logger log = LoggerFactory.getLogger(SetAuthorshipWorkflowProcess.class);

    @Override
    public void execute(WorkItem workItem, WorkflowSession workflowSession,
                        MetaDataMap metaDataMap) throws WorkflowException {

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

        try (ResourceResolver resolver = workflowSession.adaptTo(ResourceResolver.class)) {

            // Workflow runs as the initiating user on Author — session is intact
            String initiatorId = workItem.getWorkflow().getInitiator();

            Resource jcrContent = resolver.getResource(payloadPath + "/jcr:content");
            if (jcrContent == null) {
                log.warn("jcr:content not found at {}", payloadPath);
                return;
            }

            ModifiableValueMap props = jcrContent.adaptTo(ModifiableValueMap.class);
            if (props == null) return;

            // Set ww:createdBy only if not already present
            if (!props.containsKey("ww:createdBy")) {
                String originalAuthor = props.get("jcr:createdBy", initiatorId);
                props.put("ww:createdBy", originalAuthor);
            }

            // Always update ww:lastPublishedBy when workflow fires on activation
            props.put("ww:lastPublishedBy", initiatorId);

            resolver.commit();
            log.info("Authorship set: ww:createdBy={} on {}", 
                      props.get("ww:createdBy", String.class), payloadPath);

        } catch (Exception e) {
            throw new WorkflowException("Could not set authorship property", e);
        }
    }
}

Option C — ReplicationAction Event (Pre-Distribution Hook)

If you want to stamp ww:lastPublishedBy on every publish action without a full workflow, listen for the ReplicationAction OSGi event which fires on Author just before the package is dispatched:

Java — ReplicationAction Listener
package com.acme.core.listeners;

import com.day.cq.replication.ReplicationAction;
import com.day.cq.replication.ReplicationActionType;
import org.apache.sling.api.resource.*;
import org.osgi.service.component.annotations.*;
import org.osgi.service.event.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.*;

@Component(
    service = EventHandler.class,
    property = { "event.topics=" + ReplicationAction.EVENT_TOPIC }
)
public class PublishAuthorshipListener implements EventHandler {

    private static final Logger log = LoggerFactory.getLogger(PublishAuthorshipListener.class);

    @Reference
    private ResourceResolverFactory resolverFactory;

    @Override
    public void handleEvent(Event event) {
        ReplicationAction action = ReplicationAction.fromEvent(event);
        if (action == null || action.getType() != ReplicationActionType.ACTIVATE) return;

        String path        = action.getPath();
        String publishedBy = action.getUserId();

        Map authInfo = Collections.singletonMap(
            ResourceResolverFactory.SUBSERVICE, "authorship-service"
        );

        try (ResourceResolver resolver = resolverFactory.getServiceResourceResolver(authInfo)) {

            Resource jcrContent = resolver.getResource(path + "/jcr:content");
            if (jcrContent == null) return;

            ModifiableValueMap props = jcrContent.adaptTo(ModifiableValueMap.class);
            if (props == null) return;

            // Stamp original author only on first publish
            if (!props.containsKey("ww:createdBy")) {
                String creator = props.get("jcr:createdBy", publishedBy);
                props.put("ww:createdBy", creator);
            }

            // Update publisher identity on every activation
            props.put("ww:lastPublishedBy", publishedBy);
            resolver.commit();

        } catch (Exception e) {
            log.error("Failed to record publish authorship for {}", path, e);
        }
    }
}
Which Option to Choose?
  • Option A (ResourceChangeListener) — Best for capturing the true original creator at page-creation time. The author's identity is captured before any publish ever happens.
  • Option B (Workflow) — Best when you already have a DAM/Page creation workflow. Easy to add as a step. Slightly delayed vs. the creation event.
  • Option C (ReplicationAction) — Best for capturing publisher identity on every activation. Works alongside Option A for a complete picture.

7. Exposing Authorship in Sling Model and JSON Exporter

Update your Sling Model to read the custom property on Publish instead of relying on the JCR-managed one:

Java — Sling Model with JSON Exporter
package com.acme.core.models;

import com.adobe.cq.export.json.ExporterConstants;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ValueMap;
import org.apache.sling.models.annotations.*;
import org.apache.sling.models.annotations.injectorspecific.ValueMapValue;
import org.apache.sling.models.annotations.injectorspecific.ChildResource;

import javax.annotation.PostConstruct;

@Model(
    adaptables = Resource.class,
    defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL,
    resourceType = "acme/components/page",
    exporterName = ExporterConstants.SLING_MODEL_EXPORTER_NAME,
    extensions  = ExporterConstants.SLING_MODEL_EXTENSION
)
public class PageMetaModel {

    @ChildResource(name = "jcr:content")
    private Resource jcrContent;

    private String author;
    private String lastPublishedBy;
    private String lastPublished;

    @PostConstruct
    protected void init() {
        if (jcrContent == null) return;

        ValueMap props = jcrContent.getValueMap();

        // ✓ Read custom property — reliable on both Author and Publish
        author = props.get("ww:createdBy", String.class);

        // Fallback chain: custom → cq:lastReplicatedBy → "Unknown"
        if (author == null || author.isBlank()) {
            author = props.get("cq:lastReplicatedBy",
                        props.get("jcr:createdBy", "Unknown"));
        }

        lastPublishedBy = props.get("ww:lastPublishedBy",
                            props.get("cq:lastReplicatedBy", "Unknown"));

        lastPublished = props.get("cq:lastReplicated",
                          props.get("jcr:lastModified", ""));
    }

    /**
     * JSON key: "author"
     * Returns the original content creator, reliable on Publish.
     */
    @Nonnull
    public String getAuthor() {
        return author != null ? author : "Unknown";
    }

    /**
     * JSON key: "lastPublishedBy"
     */
    @Nonnull
    public String getLastPublishedBy() {
        return lastPublishedBy != null ? lastPublishedBy : "Unknown";
    }

    /**
     * JSON key: "lastPublished"
     */
    @Nonnull
    public String getLastPublished() {
        return lastPublished != null ? lastPublished : "";
    }
}

Expected JSON Output on Publish

JSON
{
  "title": "Homepage",
  "author": "[email protected]",
  "lastPublishedBy": "[email protected]",
  "lastPublished": "2026-04-13T10:30:00.000Z",
  ":type": "acme/components/page"
}

The author field now correctly reflects the original content creator on Publish — regardless of which service user performed the distribution import.

8. Common Pitfalls

  • Reading jcr:createdBy on Publish in production APIs. This is the root mistake. On Publish, content is created by the distribution mechanism, so jcr:createdBy may reflect a system user such as the distribution service — not the original content author. Any headless endpoint, Sling Model, or GraphQL resolver that reads it directly from the Publish JCR will return the wrong identity.
  • Running the backfill migration on Publish. The jcr:createdBy value on Publish is already sling-distribution-importer. Backfilling from it propagates the wrong value into your custom property.
  • Treating cq:lastReplicatedBy as the creator. This property records who last published — not who originally created the page. The same person may have published someone else's content.
  • Overwriting ww:createdBy on subsequent edits or publishes. The custom creator property must be immutable once set. Overwriting it on every save or publish defeats the purpose — it will end up reflecting the last editor, not the original author.
  • Assuming this is fixed by switching replication strategies. This behaviour is inherent to the JCR specification and applies to all content distribution mechanisms in AEM — classic replication, SCD, and the Content Transfer Tool. There is no configuration switch to make protected properties survive a cross-repository import.
  • Forgetting to include the custom property in Content Transfer Tool migrations. When migrating from AEM 6.5 to Cloud, verify that custom authorship properties are included in your inclusion sets. They are application properties and need explicit inclusion if you're using path-based filters.

9. Key Takeaways — Do's and Don'ts

01

Not a bug. jcr:createdBy changing on Publish is correct JCR behaviour. The distribution service user creates the node on Publish — the repository records it accurately.

02

Protected properties cannot cross the distribution boundary. No AEM configuration or code can override jcr:createdBy on Publish. The specification prohibits it.

03

Custom properties survive. Non-protected custom properties (e.g., ww:createdBy) travel unchanged through the distribution package to Publish.

04

Set it on Author, read it anywhere. Capture the author identity at page-creation time on Author using a listener or workflow. It will be available on Publish after the first distribution.

Do

  • Use ww:createdBy (or equivalent) for authorship in APIs and UIs
  • Capture the author at page-creation time via ResourceChangeListener or workflow
  • Use cq:lastReplicatedBy for "last published by" attribution
  • Run backfill migrations on Author only, then re-publish
  • Treat ww:createdBy as immutable — write once, never overwrite
  • Document this behaviour for developers joining the project

Don't

  • Read jcr:createdBy directly on Publish in any API or report
  • Assume this is a bug and raise an Adobe support case expecting a fix
  • Run backfill scripts on Publish (wrong values will be captured)
  • Confuse "publisher" (cq:lastReplicatedBy) with "creator"
  • Overwrite ww:createdBy on subsequent edits or publishes
  • Rely on jcr:lastModifiedBy on Publish for audit trails

10. Frequently Asked Questions

Does AEM Cloud change jcr:createdBy after publishing a page? +
Yes — but not in the way you might expect. AEM Cloud does not modify the existing jcr:createdBy value. What actually happens is that the Publish instance treats the incoming content as a brand-new node being created for the first time in its own JCR repository. The repository then sets jcr:createdBy itself, using the active session at the time of import — which is always the sling-distribution-importer service user. So the value is not "changed"; it is "set fresh" on Publish, independently of whatever value existed on Author. This distinction matters: the Author value is not overwritten — it remains intact on Author. The Publish instance simply never receives the Author value because protected JCR properties are excluded from distribution packages by design.
Is this a bug in AEM Cloud Service? +
No. It is the correct and expected behaviour based on the JCR 2.0 specification. jcr:createdBy is a protected property set by the repository at node-creation time. On Publish, the node is created by the distribution importer service user, so the repository records that user. This is not a defect in AEM's implementation.
Will Adobe fix this or provide a built-in solution? +
Unlikely, because it would require violating the JCR specification. Adobe cannot make the repository override protected properties during package import without breaking the spec guarantee. The recommended pattern (custom non-protected property) is the architecturally correct solution.
Does this only affect AEM Cloud? What about AEM 6.5? +
The same behaviour exists in AEM 6.5 and Managed Services. The service user name differs — in classic replication it is the replication transport user (e.g., replication-receiver) rather than sling-distribution-importer. The root cause and solution are identical.
Can I use cq:lastReplicatedBy instead of a custom property? +
cq:lastReplicatedBy is a good built-in substitute for publisher identity and requires no custom code. However, it records who last triggered the publish action — not necessarily who originally created the page. If those are always the same person in your workflow, it may be sufficient. For proper creator attribution, a custom immutable property is more reliable.
What happens if a page is deleted and recreated on Publish (e.g., full content re-import)? +
On re-creation, jcr:createdBy will again reflect the distribution mechanism's service user, since the node is created fresh in the Publish repository. Your custom ww:createdBy property will survive because it travels in the distribution package from Author each time. This is another argument in favour of the custom property approach — it is resilient to full re-imports.
Can I use AEM's Content Fragment author field as an alternative? +
Content Fragments have their own metadata model. For page-level authorship on cq:Page nodes, the custom property approach on jcr:content is the correct pattern. Content Fragments store author metadata differently and are not directly comparable.

Related Topics

AEM Cloud jcr:createdBy sling-distribution-importer Sling Content Distribution AEM Publish behavior AEM metadata issue AEM headless API AEM architecture JCR protected properties cq:lastReplicatedBy AEM service user AEM Workflow ResourceChangeListener Adobe Experience Manager AEMaaCS WegWorks Weg Works

Building on AEM Cloud and Running Into Architecture Issues?

WegWorks specialises in AEM Cloud Service implementations, headless API design, and content distribution architecture. If your team is wrestling with metadata, audit workflows, or distribution behaviour, we can help.

Talk to an AEM Architect