AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
SKILL verified Apache-2.0 Self-run

Event Migration

skill-adobe-skills-event-migration · by adobe

AEM Cloud Service expert skill for OSGi Event Admin handlers (non-resource events). Covers migration of javax.jcr.observation.EventListener (residual non-resource cases) and OSGi EventHandler with inline business logic to the lightweight EventHandler + JobConsumer split. Includes routing rules (resource events go to resource-change-listener, external notification goes to AEM Eventing), TopologyEv…

No reviews yet
0 installs
29 views
0.0% view→install

Install

$ agentstack add skill-adobe-skills-event-migration

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/skill-adobe-skills-event-migration)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
2mo ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Event Migration? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

Event Migration — AEM as a Cloud Service

Overview

org.osgi.service.event.EventHandler is the API for reacting to non-resource OSGi Event Admin events on AEM CS — replication events (com.day.cq.replication.ReplicationEvent.EVENT_TOPIC), workflow events (com/adobe/granite/workflow/*), and custom inter-bundle event topics. The handler runs on a shared OSGi event thread and must stay lightweight — all business logic must be offloaded to a Sling Job via JobManager.addJob().

Three CS-specific constraints every event handler must satisfy:

| Constraint | Why | |-----------|-----| | No ResourceResolver / Session / JCR ops inside handleEvent() | Blocks the shared OSGi event-admin thread; can delay every other registered handler | | getServiceResourceResolver(SUBSERVICE) in the consumer | getAdministrativeResourceResolver is removed from the CS SDK | | Leader-only handlers must implement TopologyEventListener and check isLeader | A simple run-mode check fires on every author cluster pod — TopologyEventListener is the supported way to elect a single leader |

> handleEvent() runs synchronously on a shared OSGi thread. Doing repository, network, or workflow work inline blocks event delivery for every other handler subscribed to anything. Always offload to a JobConsumer.


Routing — is this the right skill?

Use this skill when the source listens to a non-resource OSGi Event Admin topic:

| Source topic | Description | |--------------|-------------| | com.day.cq.replication.ReplicationEvent.EVENT_TOPIC | Replication actions (activate, deactivate, etc.) | | com/adobe/granite/workflow/* | Workflow lifecycle events | | Custom OSGi topics posted by another bundle | Inter-bundle event signaling | | Other non-resource OSGi events | Any OSGi event that is not a resource-change topic |

Route elsewhere when:

| Source observes… | Use instead | |------------------|-------------| | Repository content (page, asset, property added/changed/removed) | resource-change-listener skillResourceChangeListener is the supported API on CS | | External system notification (Adobe I/O Events, App Builder, webhooks, downstream services) | AEM Eventing — cloud-native, runs outside AEM, distinct from OSGi Event Admin | | Anything subscribed to org/apache/sling/api/resource/Resource/* | resource-change-listener skill — those OSGi resource topics are a deprecated internal Sling dispatcher detail |

Do not subscribe a new EventHandler to org/apache/sling/api/resource/Resource/ADDED|CHANGED|REMOVED. Those topics are an internal Sling dispatcher detail, deprecated as an application-facing API. Use ResourceChangeListener for resource observation.

Where to find topic constants

Always subscribe via a documented topic constant — never invent or copy-paste a topic string. The canonical constants live in the producing bundle's API:

| Producing API | Constant | |---------------|----------| | Replication | com.day.cq.replication.ReplicationEvent.EVENT_TOPIC | | Workflow | com.adobe.granite.workflow.event.WorkflowEvent.EVENT_TOPIC — combine with WorkflowEvent.EVENT_TYPE property to discriminate the sub-event | | Sling Jobs (lifecycle) | org.apache.sling.event.jobs.NotificationConstants.* | | Custom bundle topics | Check the producing bundle's source or documentation for its declared static final String topic constant |

A typo'd topic string compiles fine and silently subscribes to a topic that nothing posts to — the handler is ACTIVE but never fires. Reference the constant in code instead of duplicating the literal string.


Classification — choose before making any changes

Implements EventHandler for a non-resource topic and handleEvent() only enqueues jobs → Already compliant — verify against the [Review Checklist](#review-checklist) only.

Implements EventHandler for a non-resource topic and handleEvent() contains business logic (resolver, JCR ops, heavy processing) → Apply E1–E5.

Implements javax.jcr.observation.EventListener for a concern that cannot be expressed as a ResourceChangeListener (rare — most legacy JCR listeners should route to the resource-change-listener skill) → Apply E0 (convert to EventHandler for the appropriate non-resource topic) then E1–E5. If unsure whether RCL covers it, prefer RCL and ask before falling through to this skill.

One pattern per session. If the bundle has multiple legacy handlers, 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 (replication EventHandler with inline business logic)

package com.example.listeners;

import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Reference;
import org.apache.sling.api.resource.ModifiableValueMap;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.osgi.service.event.Event;
import org.osgi.service.event.EventConstants;
import org.osgi.service.event.EventHandler;
import com.day.cq.replication.ReplicationAction;
import com.day.cq.replication.ReplicationActionType;
import com.day.cq.replication.ReplicationEvent;

import java.util.Calendar;
import java.util.Collections;

@Component(
    immediate = true,
    property = {
        EventConstants.EVENT_TOPIC + "=com/day/cq/replication"
    }
)
public class ReplicationDateEventHandler implements EventHandler {

    @Reference
    private ResourceResolverFactory resolverFactory;

    @Override
    public void handleEvent(Event event) {
        try {
            ReplicationAction action = ReplicationEvent.fromEvent(event).getReplicationAction();
            if (action.getType() == ReplicationActionType.ACTIVATE) {
                ResourceResolver resolver = resolverFactory.getServiceResourceResolver(
                        Collections.singletonMap(ResourceResolverFactory.SUBSERVICE, "replication-service"));
                Resource resource = resolver.getResource(action.getPath() + "/jcr:content");
                if (resource != null) {
                    ModifiableValueMap map = resource.adaptTo(ModifiableValueMap.class);
                    map.put("cq:lastReplicated", Calendar.getInstance());
                    resolver.commit();
                }
                resolver.close();
            }
        } catch (Exception e) {
            System.err.println("Error updating replication date: " + e.getMessage());
            e.printStackTrace();
        }
    }
}

After — Cloud Service compatible

File 1 — ReplicationDateEventHandler.java (lightweight EventHandler + leader election)

package com.example.listeners;

import org.apache.sling.discovery.TopologyEvent;
import org.apache.sling.discovery.TopologyEventListener;
import org.apache.sling.event.jobs.JobManager;
import org.osgi.framework.Constants;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.event.Event;
import org.osgi.service.event.EventConstants;
import org.osgi.service.event.EventHandler;
import com.day.cq.replication.ReplicationAction;
import com.day.cq.replication.ReplicationActionType;
import com.day.cq.replication.ReplicationEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.HashMap;
import java.util.Map;

@Component(
    service = { EventHandler.class, TopologyEventListener.class },
    immediate = true,
    property = {
        Constants.SERVICE_DESCRIPTION + "=Update lastReplicated on activation",
        EventConstants.EVENT_TOPIC + "=com/day/cq/replication"
    }
)
public class ReplicationDateEventHandler implements EventHandler, TopologyEventListener {

    public static final String JOB_TOPIC = "com/example/replication/date/update";

    private static final Logger LOG = LoggerFactory.getLogger(ReplicationDateEventHandler.class);
    private volatile boolean isLeader = false;

    @Reference
    private JobManager jobManager;

    @Override
    public void handleTopologyEvent(TopologyEvent event) {
        if (event.getType() == TopologyEvent.Type.TOPOLOGY_CHANGED
                || event.getType() == TopologyEvent.Type.TOPOLOGY_INIT) {
            isLeader = event.getNewView().getLocalInstance().isLeader();
            LOG.info("Topology updated; isLeader={}", isLeader);
        }
    }

    @Override
    public void handleEvent(Event event) {
        if (!isLeader) {
            return;
        }
        try {
            ReplicationAction action = ReplicationEvent.fromEvent(event).getReplicationAction();
            if (action.getType() != ReplicationActionType.ACTIVATE) {
                return;
            }
            Map jobProperties = new HashMap<>();
            jobProperties.put("path", action.getPath());
            jobManager.addJob(JOB_TOPIC, jobProperties);
        } catch (Exception e) {
            LOG.error("Failed to enqueue replication-date job", e);
        }
    }
}

File 2 — ReplicationDateJobConsumer.java (business logic runs here, with a service-user resolver)

package com.example.listeners;

import org.apache.sling.api.resource.LoginException;
import org.apache.sling.api.resource.ModifiableValueMap;
import org.apache.sling.api.resource.PersistenceException;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;
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.Calendar;
import java.util.Collections;

@Component(
    service = JobConsumer.class,
    property = {
        JobConsumer.PROPERTY_TOPICS + "=com/example/replication/date/update"
    }
)
public class ReplicationDateJobConsumer implements JobConsumer {

    private static final Logger LOG = LoggerFactory.getLogger(ReplicationDateJobConsumer.class);
    private static final String SUBSERVICE = "event-handler-service";

    @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))) {

            Resource resource = resolver.getResource(path + "/jcr:content");
            if (resource != null) {
                ModifiableValueMap map = resource.adaptTo(ModifiableValueMap.class);
                map.put("cq:lastReplicated", Calendar.getInstance());
                resolver.commit();
                LOG.debug("Updated lastReplicated for {}", path);
            }
            return JobResult.OK;

        } catch (LoginException e) {
            LOG.error("Could not open service resolver for '{}'", SUBSERVICE, e);
            return JobResult.FAILED;
        } catch (PersistenceException e) {
            LOG.error("Failed to commit lastReplicated for {}", path, e);
            return JobResult.FAILED;
        }
    }
}

Required Repoinit (in ui.config):

create service user event-handler-service

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

Required service-user mapping (ui.config, file named e.g. org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended-event-handler.cfg.json):

{
  "user.mapping": [
    "com.example.mybundle:event-handler-service=[event-handler-service]"
  ]
}

Use the amend form (ServiceUserMapperImpl.amended-.cfg.json) and the principal-name mapping (square-bracket form =[service-user-name]). Do not use the deprecated userName form. Permissions must be declared directly on the service user — do not rely on group inheritance.

Variant — workflow events

Workflow events live on topics under com/adobe/granite/workflow/* and expose payload properties via com.adobe.granite.workflow.event.WorkflowEvent constants. The overall shape (lightweight handler + leader election + JobConsumer) is identical to the replication example — only the topic, the event-type discriminator, and the payload keys change. Full standalone handler:

package com.example.listeners;

import com.adobe.granite.workflow.event.WorkflowEvent;
import org.apache.sling.discovery.TopologyEvent;
import org.apache.sling.discovery.TopologyEventListener;
import org.apache.sling.event.jobs.JobManager;
import org.osgi.framework.Constants;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.event.Event;
import org.osgi.service.event.EventConstants;
import org.osgi.service.event.EventHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.HashMap;
import java.util.Map;

@Component(
    service = { EventHandler.class, TopologyEventListener.class },
    immediate = true,
    property = {
        Constants.SERVICE_DESCRIPTION + "=Handle workflow completion",
        EventConstants.EVENT_TOPIC + "=com/adobe/granite/workflow/event"
    }
)
public class WorkflowCompletedEventHandler implements EventHandler, TopologyEventListener {

    public static final String JOB_TOPIC = "com/example/workflow/completed";

    private static final Logger LOG = LoggerFactory.getLogger(WorkflowCompletedEventHandler.class);
    private volatile boolean isLeader = false;

    @Reference
    private JobManager jobManager;

    @Override
    public void handleTopologyEvent(TopologyEvent event) {
        if (event.getType() == TopologyEvent.Type.TOPOLOGY_CHANGED
                || event.getType() == TopologyEvent.Type.TOPOLOGY_INIT) {
            isLeader = event.getNewView().getLocalInstance().isLeader();
        }
    }

    @Override
    public void handleEvent(Event event) {
        if (!isLeader) {
            return;
        }
        try {
            String eventType = (String) event.getProperty(WorkflowEvent.EVENT_TYPE);
            if (!WorkflowEvent.WORKFLOW_COMPLETED_EVENT.equals(eventType)) {
                return;
            }
            Map jobProperties = new HashMap<>();
            jobProperties.put("workflowId", event.getProperty(WorkflowEvent.WORKFLOW_ID));
            jobProperties.put("workItemId", event.getProperty(WorkflowEvent.WORK_ITEM));
            jobProperties.put("path", event.getProperty("path"));
            jobManager.addJob(JOB_TOPIC, jobProperties);
        } catch (Exception e) {
            LOG.error("Failed to enqueue workflow-completed job", e);
        }
    }
}

Common WorkflowEvent payload keys:

| Key | Purpose | |-----|---------| | WorkflowEvent.EVENT_TYPE | Event subtype — WORKFLOW_STARTED_EVENT, WORKFLOW_COMPLETED_EVENT, WORKFLOW_ABORTED_EVENT, WORK_ITEM_COMPLETED_EVENT, etc. | | WorkflowEvent.WORKFLOW_ID | The workflow instance ID | | WorkflowEvent.WORK_ITEM | The current work item ID | | path | The payload path the workflow is operating on (when applicable) |

The JobConsumer pattern is unchanged from the replication example — read the properties via job.getProperty("key", Type.class) and apply the business logic.


E0 — Conver

Source & license

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

  • Author: adobe
  • Source: 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.

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.