Syncing Experience Fragments as Content Blocks

Introduction

Adobe Experience Manager (AEM) is an enterprise content management platform that enables organizations to create, manage, and deliver personalized digital experiences across multiple channels. 

AEM <> MoEngage

The integration connects AEM to MoEngage via a custom workflow process, allowing real-time content synchronization from AEM to MoEngage Content Blocks. The integration enables marketing teams to manage content centrally in AEM while leveraging it in MoEngage campaigns for personalized customer engagement.

Use Cases

Integrating Adobe Experience Manager with MoEngage helps you with the following use cases:

  • Campaign Content Management: Marketing teams create promotional content in AEM. When published, it automatically syncs to MoEngage Content Blocks, making it immediately available for email campaigns, push notifications, and in-app messages.
  • Personalized Experience Delivery: Create localized Experience Fragments in AEM with personalization tokens. These automatically sync with MoEngage, utilizing converted tokens to enable personalized campaigns based on user attributes.
  • Dynamic Content Updates: Update product descriptions, offers, or messaging in AEM. Changes automatically propagate to all active MoEngage campaigns using those Content Blocks, ensuring consistency across channels.
  • Multi-Channel Content Reuse: Author content once in AEM and use it across multiple MoEngage campaigns (email, push, in-app) without duplication or manual copying.

Integration

Approach

Implement a custom integration within AEM and use custom workflows to an API call to MoEngage Content Block APIs whenever relevant content is created, updated, or published. This ensures MoEngage has access to the latest AEM content. 

Flow diagram 

We will to develop a custom AEM Workflow Process Step that connects to the MoEngage APIs. 

Supported Content Types

The following table outlines the content types that can be synced from AEM to MoEngage:

Content Type Description MoEngage Sync Type
Experience Fragments Reusable content components with HTML, text, and media Content Block (HTML)
Content Fragments Structured content with predefined models and elements Content Block (HTML/Text)
DAM Assets (.html, .txt) Text or HTML files stored in Digital Asset Manager Content Block (HTML/Text)

Supported Actions 

Actions in AEM Results in MoEngage
Publish  Creates/Updates Content Blocks (status: ACTIVE)
library_add_check

Prerequisites

Before you begin, ensure you have the following requirements:

  • Active Adobe Experience Manager instance (AEM 6.5 or AEM as a Cloud Service).
  • AEM Author and Publish environments configured.
  • Maven project setup with appropriate permissions to deploy OSGi bundles.
  • MoEngage workspace with API access enabled.
  • MoEngage Workspace ID (API Key) and Data API Secret.
  • Java development environment with AEM SDK.
  • Understanding of AEM Workflows and OSGi services.

Step 1: Get your MoEngage API Credentials

To find your credentials, perform the following steps:

  1. On the left navigation menu in the MoEngage UI, click Settings > Account > APIs.
  2. On the APIs page, copy the following credentials:
    1. Workspace ID (earlier app id): You can use this as API Key (MOE-APPKEY header)
    2. Campaign API Secret: You can use this as Basic Authentication
  3. Find your data center code from your dashboard's URL. For more information, refer Data Centers in MoEngage.

Step 2: Update Maven Project Dependencies

To update Maven project dependencies, perform the following steps:

  1. Add the org.json library to your AEM project's core/pom.xml file to ensure proper JSON payload construction.
  2. Locate the <dependencies> section in core/pom.xml.
  3. Add the following dependency:
     
<dependency>
    <groupId>org.json</groupId>
    <artifactId>json</artifactId>
    <version>20231013</version>
    <scope>compile</scope>
</dependency>


Why org.json? 

The org.json library automatically escapes special characters (quotes, newlines) in HTML content, preventing JSON payload errors. Manual string concatenation can break the MoEngage API call if content contains these characters.

Step 3: Setting up the Custom Workflow  

You need to create an OSGi service that handles content sync to MoEngage. The service implements the WorkflowProcess interface and registers as a workflow step. You can refer to the sample shared below or create one of your own. 
 

Sample production ready implementation 

This implementation covers the full lifecycle management: 

  • Handles Experience Fragments with variations.
  • Supports Content Fragments and DAM Assets.
  • Debug mode with webhook support for testing.
  • Improved HTML cleaning and link externalization.

You can just copy this code and add it to a file path- core/src/main/java/com/[your-company]/integration/workflow/MoEngageContentSyncStep.java

Sample Production-Ready Java Implementation
package com.moengage.integration.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.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.engine.SlingRequestProcessor;
import com.day.cq.contentsync.handler.util.RequestResponseFactory;
import com.day.cq.dam.api.Asset;
import com.day.cq.dam.api.Rendition;
import com.day.cq.wcm.api.Page;
import com.adobe.cq.dam.cfm.ContentFragment;
import com.adobe.cq.dam.cfm.ContentElement;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Collectors;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.json.JSONObject;
import org.json.JSONArray;

@Component(
    service = WorkflowProcess.class,
    property = { "process.label=MoEngage Content Sync" }
)
public class MoEngageContentSyncStep implements WorkflowProcess {

    private static final Logger log = LoggerFactory.getLogger(MoEngageContentSyncStep.class);
    private static final String API_BASE_TEMPLATE = "https://api-%s.moengage.com/v1/external/campaigns/content-blocks";
    private static final int BUFFER_DELAY_MS = 500;

    @Reference private RequestResponseFactory requestResponseFactory;
    @Reference private SlingRequestProcessor requestProcessor;

    @Override
    public void execute(WorkItem workItem, WorkflowSession workflowSession, MetaDataMap args) 
            throws WorkflowException {
        ResourceResolver resourceResolver = null;
        
        try {
            String processArgs = args.get("PROCESS_ARGS", "string");
            Map<String, String> config = parseProcessArgs(processArgs);
            
            String apiKey = config.get("moengage.api.key");
            String apiSecret = config.get("moengage.api.secret");
            String dataCenter = config.get("moengage.datacenter");
            String publishUrl = config.get("aem.publish.url");
            String targetStatus = config.get("moengage.status");
            boolean debugMode = Boolean.parseBoolean(config.get("moengage.debug"));
            String debugUrl = config.get("moengage.debug.url");

            if (apiKey == null || apiKey.trim().isEmpty()) {
                log.warn("MoEngage API key not configured, skipping");
                return;
            }
            if (apiSecret == null) apiSecret = "";
            if (dataCenter == null || dataCenter.trim().isEmpty()) {
                log.warn("MoEngage datacenter not configured, skipping");
                return;
            }
            if (targetStatus == null) targetStatus = "ACTIVE";
            targetStatus = targetStatus.toUpperCase();
            
            if (publishUrl != null && publishUrl.endsWith("/")) {
                publishUrl = publishUrl.substring(0, publishUrl.length() - 1);
            }

            String basicAuthHeader = "Basic " + Base64.getEncoder()
                .encodeToString((apiKey.trim() + ":" + apiSecret.trim())
                .getBytes(StandardCharsets.UTF_8));
            String baseUrl = String.format(API_BASE_TEMPLATE, dataCenter.trim());
            String searchUrl = baseUrl + "/search";

            resourceResolver = workflowSession.adaptTo(ResourceResolver.class);
            if (resourceResolver == null) {
                log.error("Could not obtain ResourceResolver");
                return;
            }

            String payloadPath = getCleanPath(workItem.getWorkflowData()
                .getPayload().toString());
            Resource payloadResource = resourceResolver.getResource(payloadPath);

            if (payloadResource == null) {
                log.warn("Resource not found: {}", payloadPath);
            return;
            }

            String initiator = workItem.getWorkflow().getInitiator();
            
            if (payloadPath.contains("/experience-fragments/")) {
                processExperienceFragments(payloadResource, resourceResolver, publishUrl,
                    baseUrl, searchUrl, apiKey.trim(), basicAuthHeader, targetStatus,
                    initiator, debugMode, debugUrl);
            } else if (payloadPath.contains("/content/dam/")) {
                processDAMContent(payloadResource, resourceResolver, publishUrl,
                    baseUrl, searchUrl, apiKey.trim(), basicAuthHeader, targetStatus,
                    initiator, debugMode, debugUrl);
            } else {
                log.info("Unsupported content type at path: {}", payloadPath);
            }

        } catch (Exception e) {
            log.error("MoEngage Sync Failed", e);
        }
    }

    private void processExperienceFragments(Resource resource, ResourceResolver resolver,
            String publishUrl, String baseUrl, String searchUrl, String apiKey,
            String basicAuthHeader, String targetStatus, String initiator,
            boolean debugMode, String debugUrl) throws Exception {
        
        List<XFItem> items = collectExperienceFragments(resource, resolver);
        
        if (items.isEmpty()) {
            log.info("No experience fragments found");
            return;
        }

        for (int i = 0; i < items.size(); i++) {
            XFItem item = items.get(i);
            
            try {
                processExperienceFragment(item, resolver, publishUrl, baseUrl,
                    searchUrl, apiKey, basicAuthHeader, targetStatus, initiator,
                    debugMode, debugUrl);
                
                if (i < items.size() - 1) {
                    Thread.sleep(BUFFER_DELAY_MS);
                }
            } catch (Exception e) {
                log.error("Failed to process XF: {}", item.path, e);
            }
        }
    }

    private List<XFItem> collectExperienceFragments(Resource resource, 
            ResourceResolver resolver) {
        List<XFItem> items = new ArrayList<>();
        
        if (resource == null) return items;
        
        Page page = resource.adaptTo(Page.class);
        
        if (page != null) {
            Iterator<Page> children = page.listChildren();
            boolean hasChildren = children.hasNext();
            
            if (hasChildren) {
                children = page.listChildren();
                while (children.hasNext()) {
                    Page variation = children.next();
                    items.add(new XFItem(variation.getPath(), page, variation));
                }
            } else {
                Page parent = page.getParent();
                if (parent != null && parent.listChildren().hasNext()) {
                    items.add(new XFItem(page.getPath(), parent, page));
                }
            }
        } else {
            Iterator<Resource> children = resource.listChildren();
            while (children.hasNext()) {
                items.addAll(collectExperienceFragments(children.next(), resolver));
            }
        }
        
        return items;
    }

    private void processExperienceFragment(XFItem item, ResourceResolver resolver,
            String publishUrl, String baseUrl, String searchUrl, String apiKey,
            String basicAuthHeader, String targetStatus, String initiator,
            boolean debugMode, String debugUrl) throws IOException {
        
        String itemName = generatePathBasedName(item.path);
        String rawContent = renderPageHtml(item.variation.getPath(), resolver);
        
        rawContent = cleanContent(rawContent);
        
        if (publishUrl != null && !publishUrl.isEmpty()) {
            rawContent = externalizeLinks(rawContent, publishUrl);
        }
        
        if (rawContent.isEmpty()) {
            log.info("Empty content for XF: {}, skipping", item.path);
            return;
        }
        
        itemName = sanitizeName(itemName);
        
        log.info("Processing XF: {} -> {}", item.path, itemName);
        
        String existingId = searchContentBlockId(searchUrl, apiKey, basicAuthHeader,
            itemName, debugMode, debugUrl);
        
        JSONObject payload = new JSONObject();
        
        if (existingId != null) {
            payload.put("id", existingId);
            payload.put("raw_content", rawContent);
            payload.put("updated_by", initiator);
            payload.put("status", targetStatus);
            payload.put("description", "Sync from AEM: " + item.path);
            sendRequest(baseUrl, "PUT", apiKey, basicAuthHeader,
                payload.toString(), debugMode, debugUrl);
            log.info("Updated content block: {}", itemName);
        } else {
            payload.put("name", itemName);
            payload.put("label", itemName);
            payload.put("content_type", "HTML");
            payload.put("raw_content", rawContent);
            payload.put("status", targetStatus);
            payload.put("created_by", initiator);
            payload.put("images_used", new JSONArray());
            payload.put("content_block_used", new JSONArray());
            payload.put("tag_ids", new JSONArray());
            sendRequest(baseUrl, "POST", apiKey, basicAuthHeader,
                payload.toString(), debugMode, debugUrl);
            log.info("Created content block: {}", itemName);
        }
    }

    private void processDAMContent(Resource resource, ResourceResolver resolver,
            String publishUrl, String baseUrl, String searchUrl, String apiKey,
            String basicAuthHeader, String targetStatus, String initiator,
            boolean debugMode, String debugUrl) throws IOException {
        
        String itemName = generatePathBasedName(resource.getPath());
        String rawContent = "";
        String contentType = "HTML";
        
        ContentFragment cf = resource.adaptTo(ContentFragment.class);
        if (cf != null) {
            itemName = cf.getName();
            rawContent = extractContentFromFragment(cf);
        } else {
            Asset asset = resource.adaptTo(Asset.class);
            if (asset != null) {
                itemName = asset.getName();
                String mime = asset.getMimeType();
                contentType = (mime != null && mime.contains("text/plain")) 
                    ? "Plain Text" : "HTML";
                
                Rendition original = asset.getOriginal();
                if (original != null) {
                    try (InputStream is = original.getStream()) {
                        rawContent = new BufferedReader(
                            new InputStreamReader(is, StandardCharsets.UTF_8))
                            .lines()
                            .collect(Collectors.joining("\n"));
                    }
                }
            }
        }
        
        if (rawContent.isEmpty()) {
            log.info("Empty content, skipping: {}", resource.getPath());
            return;
        }
        
        itemName = sanitizeName(itemName);
        
        log.info("Processing DAM content: {} -> {}", resource.getPath(), itemName);
        
        String existingId = searchContentBlockId(searchUrl, apiKey, basicAuthHeader,
            itemName, debugMode, debugUrl);
        
        JSONObject payload = new JSONObject();
        
        if (existingId != null) {
            payload.put("id", existingId);
            payload.put("raw_content", rawContent);
            payload.put("updated_by", initiator);
            payload.put("status", targetStatus);
            sendRequest(baseUrl, "PUT", apiKey, basicAuthHeader,
                payload.toString(), debugMode, debugUrl);
            log.info("Updated content block: {}", itemName);
        } else {
            payload.put("name", itemName);
            payload.put("label", itemName);
            payload.put("content_type", contentType);
            payload.put("raw_content", rawContent);
            payload.put("status", targetStatus);
            payload.put("created_by", initiator);
            payload.put("images_used", new JSONArray());
            payload.put("content_block_used", new JSONArray());
            payload.put("tag_ids", new JSONArray());
            sendRequest(baseUrl, "POST", apiKey, basicAuthHeader,
                payload.toString(), debugMode, debugUrl);
            log.info("Created content block: {}", itemName);
        }
    }

    private String extractContentFromFragment(ContentFragment cf) {
        if (cf.hasElement("htmlContent")) {
            return cf.getElement("htmlContent").getContent();
        }
        if (cf.hasElement("master")) {
            return cf.getElement("master").getContent();
        }
        if (cf.hasElement("body")) {
            return cf.getElement("body").getContent();
        }
        
        StringBuilder sb = new StringBuilder();
        Iterator<ContentElement> elements = cf.getElements();
        while (elements.hasNext()) {
            ContentElement el = elements.next();
            if (!isMetadataField(el.getName())) {
                sb.append(el.getContent()).append("\n");
            }
        }
        return sb.toString().trim();
    }

    private boolean isMetadataField(String name) {
        return name.matches("(?i).*(metadata|title|segment|offerCode).*");
    }


    private String generatePathBasedName(String path) {
        if (path == null || path.isEmpty()) return "content_block";
        
        String temp = path
            .replace("/content/experience-fragments/", "")
            .replace("/content/dam/", "")
            .replace("/content/", "");
        
        if (temp.contains("/jcr:content")) {
            temp = temp.substring(0, temp.indexOf("/jcr:content"));
        }
        
        return temp.replaceAll("/", "_")
                   .replaceAll("-", "_")
                   .replaceAll("[^a-zA-Z0-9_]", "")
                   .replaceAll("_+", "_")
                   .replaceAll("^_|_$", "")
                   .toLowerCase();
    }

    private String sanitizeName(String name) {
        if (name == null || name.isEmpty()) return "content_block";
        return name.replaceAll("[^a-zA-Z0-9_]", "_")
                   .replaceAll("_+", "_")
                   .replaceAll("^_|_$", "")
                   .toLowerCase();
    }

private String cleanContent(String html) {
        if (html == null || html.isEmpty()) {
            return "";
        }
        
        StringBuilder result = new StringBuilder();
        
        // 1. Capture CSS Links (ClientLibs)
        Pattern linkPattern = Pattern.compile("<link[^>]*rel=[\"']stylesheet[\"'][^>]*>", Pattern.CASE_INSENSITIVE);
        Matcher linkMatcher = linkPattern.matcher(html);
        while (linkMatcher.find()) {
            result.append(linkMatcher.group()).append("\n");
        }
        
        // 2. Capture Inline Styles
        Pattern stylePattern = Pattern.compile("<style[^>]*>.*?</style>", Pattern.DOTALL | Pattern.CASE_INSENSITIVE);
        Matcher styleMatcher = stylePattern.matcher(html);
        while (styleMatcher.find()) {
            result.append(styleMatcher.group()).append("\n");
        }
        
        // 3. Extract Body Content
        String bodyContent = html;
        Pattern bodyPattern = Pattern.compile("<body[^>]*>(.*?)</body>", Pattern.DOTALL | Pattern.CASE_INSENSITIVE);
        Matcher bodyMatcher = bodyPattern.matcher(html);
        if (bodyMatcher.find()) {
            bodyContent = bodyMatcher.group(1);
        } else {
            // Fallback: Manually strip tags. 
            // Note: We safely remove <head> here because we already captured the links in Step 1.
            bodyContent = html
                .replaceAll("(?is)<!DOCTYPE[^>]*>", "")
                .replaceAll("(?is)<html[^>]*>", "")
                .replaceAll("(?is)</html>", "")
                .replaceAll("(?is)<head[^>]*>.*?</head>", "")
                .replaceAll("(?is)<body[^>]*>", "")
                .replaceAll("(?is)</body>", "");
        }
        
        bodyContent = cleanAEMArtifacts(bodyContent);
        
        result.append(bodyContent);
        
        return result.toString().trim();
    }

    private String cleanAEMArtifacts(String content) {
        if (content == null) return "";
        
        return content
            .replaceAll("(?is)<script[^>]*src=\"/etc\\.clientlibs/[^\"]*\"[^>]*></script>", "")
            .replaceAll("\\s*data-cmp-data-layer=\"[^\"]*\"", "")
            .replaceAll("\\s*data-cmp-clickable", "")
            .replaceAll("(?is)<div[^>]*>\\s*</div>", "")
            .replaceAll("\\n\\s*\\n\\s*\\n", "\n\n")
            .trim();
    }

    private String renderPageHtml(String path, ResourceResolver resolver) {
        try {
            if (requestProcessor != null && requestResponseFactory != null) {
                HttpServletRequest req = requestResponseFactory
                    .createRequest("GET", path + ".html");
                ByteArrayOutputStream out = new ByteArrayOutputStream();
                HttpServletResponse resp = requestResponseFactory.createResponse(out);
                requestProcessor.processRequest(req, resp, resolver);
                return out.toString(StandardCharsets.UTF_8.name());
            }
        } catch (Exception e) {
            log.error("Failed to render: {}", path, e);
        }
        return "";
    }

    private String externalizeLinks(String content, String domain) {
        if (content == null || domain == null) return content;
        return content
            .replaceAll("(src|href)=\"(/content/[^\"]+)\"", "$1=\"" + domain + "$2\"")
            .replaceAll("(src|href)=\"(/etc\\.clientlibs/[^\"]+)\"",
                "$1=\"" + domain + "$2\"");
    }

    private String searchContentBlockId(String url, String apiKey, String auth,
            String name, boolean dbg, String dbgUrl) {
        try {
            JSONObject body = new JSONObject();
            JSONObject filter = new JSONObject();
            filter.put("search_text", name);
            body.put("filters", filter);
            
            APIResponse resp = sendRequest(url, "POST", apiKey, auth,
                body.toString(), dbg, dbgUrl);
            
            if (resp.statusCode >= 200 && resp.statusCode < 300) {
                JSONObject json = new JSONObject(resp.body);
                if (json.optInt("count") > 0) {
                    JSONArray data = json.getJSONArray("data");
                    for (int i = 0; i < data.length(); i++) {
                        JSONObject item = data.getJSONObject(i);
                        if (item.getString("name").equals(name)) {
                            return item.getString("id");
                        }
                    }
                }
            }
        } catch (Exception e) {
            log.debug("Search failed: {}", name, e);
        }
        return null;
    }

    private APIResponse sendRequest(String endpoint, String method, String apiKey,
            String auth, String jsonPayload, boolean dbg, String dbgUrl)
            throws IOException {
        
        if (dbg && dbgUrl != null && !dbgUrl.isEmpty()) {
            try {
                sendDebug(jsonPayload, method, dbgUrl);
            } catch (Exception e) {
                log.debug("Debug send failed", e);
            }
        }
        
        URL url = new URL(endpoint.trim());
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        conn.setRequestMethod(method);
        conn.setRequestProperty("Content-Type", "application/json");
        conn.setRequestProperty("MOE-APPKEY", apiKey);
        conn.setRequestProperty("Authorization", auth);
        conn.setConnectTimeout(30000);
        conn.setReadTimeout(30000);
        conn.setDoOutput(true);
        
        try (OutputStream os = conn.getOutputStream()) {
            os.write(jsonPayload.getBytes(StandardCharsets.UTF_8));
        }
        
        int code = conn.getResponseCode();
        String body = readResponse(conn);
        
        return new APIResponse(code, body);
    }

    private void sendDebug(String payload, String method, String debugUrl) {
        try {
            URL url = new URL(debugUrl + "?method=" + method);
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            conn.setRequestMethod("POST");
            conn.setRequestProperty("Content-Type", "application/json");
            conn.setConnectTimeout(5000);
            conn.setReadTimeout(5000);
            conn.setDoOutput(true);
            
            try (OutputStream os = conn.getOutputStream()) {
                os.write(payload.getBytes(StandardCharsets.UTF_8));
            }
            conn.getResponseCode();
        } catch (Exception e) {
            // Non-critical
        }
    }

    private String readResponse(HttpURLConnection conn) {
        try {
            InputStream is = conn.getResponseCode() >= 400 ?
                conn.getErrorStream() : conn.getInputStream();
            if (is == null) return "";
            
            try (BufferedReader reader = new BufferedReader(
                    new InputStreamReader(is, StandardCharsets.UTF_8))) {
                return reader.lines().collect(Collectors.joining("\n"));
            }
        } catch (Exception e) {
            return "";
        }
    }

    private String getCleanPath(String path) {
        if (path == null) return "";
        if (path.endsWith("/jcr:content")) {
            return path.substring(0, path.indexOf("/jcr:content"));
        }
        if (path.endsWith("/jcr:content/metadata")) {
            return path.substring(0, path.indexOf("/jcr:content/metadata"));
        }
        return path;
    }

    private Map<String, String> parseProcessArgs(String args) {
        Map<String, String> map = new HashMap<>();
        if (args != null && !args.isEmpty()) {
            for (String pair : args.split(",")) {
                String[] parts = pair.split("=", 2);
                if (parts.length == 2) {
                    map.put(parts[0].trim(), parts[1].trim());
                }
            }
        }
        return map;
    }

    private static class XFItem {
        final String path;
        final Page xfRoot;
        final Page variation;
        
        XFItem(String path, Page xfRoot, Page variation) {
            this.path = path;
            this.xfRoot = xfRoot;
            this.variation = variation;
        }
    }

    private static class APIResponse {
        final int statusCode;
        final String body;
        
        APIResponse(int statusCode, String body) {
            this.statusCode = statusCode;
            this.body = body;
        }
    }
}
info

Troubleshooting

If deployment errors occur, check the error.log file in crx-quickstart/logs/ for detailed error messages.

Step 4: Build and Deploy the OSGi Bundle

After you add the code, to build and deploy your AEM project, perform the following steps:

  1. Navigate to your project root directory.
  2. Run the Maven build command:

    mvn clean install -PautoInstallPackage
  3. Verify the bundle is active in the OSGi Console at http://<aem-author>:4502/system/console/bundles.
  4. Search for your bundle name and confirm status is Active.

Step 5: Create The Workflow Models

Create the following workflow models to handle publish operation:

Workflow 1: Publish Workflow (ACTIVE Status)

To publish workflow (ACTIVE status), perform the following steps:

  1.  Navigate to Tools > Workflow > Models in AEM Author.
  2.  Click Create > Create Model.
  3.  Enter workflow details:
    1. Title: MoEngage Content Sync
    2. Name: moengage-content-sync
  4.  Open the workflow for editing, delete the default step.
  5.  Drag the Process Step from the sidebar to the workflow canvas.
  6.  Double-click the Process Step to configure it.
  7.  In the Process dropdown, select MoEngage Content Sync.
  8.  In the Arguments field, enter:

    Plain/text
    moengage.api.key=YOUR_WORKSPACE_ID,moengage.api.secret=YOUR_API_SECRET,moengage.datacenter=YOUR_DATACENTRE_VALUE,aem.publish.url=https://your-publish-domain.com,moengage.status=ACTIVE
  9.  Click OK to save.
  10. Click Sync to save the workflow model.
     

Configuration Parameters

Parameter Description Required
moengage.api.key Your MoEngage Workspace ID Yes
moengage.api.secret Your MoEngage Data API Secret (for Basic Auth) Yes
moengage.datacenter MoEngage data center number (e.g., 02, 03, 04) Yes
aem.publish.url Base URL of your AEM Publish instance Yes
moengage.status Target status: ACTIVE Yes
moengage.debug Enable debug mode (true/false) No
moengage.debug.url Webhook URL for debugging (e.g., webhook.site) No

Step 6: Configure Workflow Launchers

Navigate to Tools → Workflow → Launchers and create 6 launchers to handle publish, unpublish, and delete operations:

Experience Fragments Launchers

Launcher Name Event Type Nodetype Path Condition Workflow
moengage-xf-publish Modified cq:PageContent /content/experience-fragments/[your-site] cq:lastReplicationAction==Activate moengage-content-sync

DAM Assets Launchers

Launcher Name Event Type Nodetype Path Condition Workflow
moengage-dam-publish Modified dam:AssetContent /content/dam/[your-site] cq:lastReplicationAction==Activate moengage-content-sync
warning

Important

For all launchers:

  • Set Run Modes: author
  • Set Enabled: true
  • Replace [your-site] with your actual site path

Step 7: Test the Integration

Enable Debug Mode (Recommended)

For initial testing, add debug parameters to workflow arguments:

moengage.debug=true,moengage.debug.url=https://webhook.site/your-unique-id

Get a free webhook URL from webhook.site to inspect payloads in real-time.

  1. Navigate to the Experience Fragment or Content Fragment in AEM Author.
  2. Select the page information icon.
  3. Click Start Workflow.
  4. Select MoEngage Content Sync Workflow.
  5. Click Start.

Personalization Token Mapping

The workflow automatically converts AEM personalization tokens to MoEngage format:

AEM Token (Adobe Target Format) MoEngage Token
{{profile.person.name.firstName}} {{UserAttribute['First Name']}}
{{profile.person.name.lastName}} {{UserAttribute['Last Name']}}
{{profile.person.name.fullName}} {{UserAttribute['Name']}}
{{identityMap.email.0.id}} {{UserAttribute['Email (Standard)']}}
{{#if condition}} {% if condition %}
{{else}} {% else %}
{{/if}} {% endif %}

Additional Resources

Support and Customization

support_agent

Support

For assistance with deployment, custom content extraction logic, troubleshooting, or advanced configurations, contact MoEngage Integration Support or your Customer Success Manager.

Previous

Next

Was this article helpful?
0 out of 0 found this helpful

How can we improve this article?