Hi Team,
I’m currently working on an implementation to enhance the Asset Timeline displayed in AEM’s assetdetails.html view.
The goal is to make the Timeline more informative, To extend the AEM Asset Timeline so that for each key asset action, it displays:
1. User ID – who performed the action
2. Timestamp – when the action occurred
3. Comment/Remarks – associated workflow or operation comment
4. Action Type – e.g. Submitted, Published, Approved, Archived, Workflow Retriggered
Example expected behavior:
If anyone had worked in enhancing the timeline need your guidance on :
1. What’s the recommended or best-practice approach to extend or customize the Asset Timeline in AEM 6.5? • For example, should it be customized via the AssetTimelineProvider, ActivityStreamService, or a custom Event Listener?
2. How can we inject custom metadata (e.g., user ID, workflow comments) into Timeline entries
3. Any official Adobe references or community articles covering Timeline customization or augmentation?
AEM Version: 6.5.21
Thanks
Topics help categorize Community content and increase your ability to discover relevant content.
Views
Replies
Total Likes
Hi @tushaar_srivastava ,
To make the Asset Timeline in AEM 6.5 show more useful details like who did what, when, and any comments, you’ll need to customize how AEM tracks and displays asset events. The best way is to create a custom Java class that extends AEM’s AssetTimelineProvider, which controls what appears in the timeline. You’ll also need to capture this extra info—like user ID and comments—during workflows or asset actions, using either a custom workflow step or an event listener. This data should be stored in the asset’s metadata so your custom provider can read and display it. This approach keeps things clean, flexible, and aligned with Adobe’s best practices.
Thanks & Regards
Vishal
Hi @VishalKa5 could you please provide the Java docs for this custom implementation because there is nothing called as AssetTimelineProvider
Views
Replies
Total Likes
Hi @tushaar_srivastava ,
Hope below code is helpful for you.
package com.myproject.aem.core.timeline;
import com.day.cq.dam.api.Asset;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.resource.*;
import org.apache.sling.api.servlets.SlingAllMethodsServlet;
import org.json.JSONArray;
import org.json.JSONObject;
import org.osgi.service.component.annotations.*;
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 javax.servlet.Servlet;
import java.io.IOException;
import java.util.*;
/**
* ===============================================================
* 💡 AEM 6.5 Enhanced Asset Timeline Implementation
* ---------------------------------------------------------------
* PART 1: Custom Event Listener → Captures user actions on assets
* PART 2: Custom Servlet → Exposes timeline data as JSON for UI
* ===============================================================
*/
public class EnhancedAssetTimeline {
// ============================================================
// 🧱 PART 1: Custom Event Listener — Tracks Asset Activities
// ============================================================
@Component(
service = EventHandler.class,
property = {
EventConstants.EVENT_TOPIC + "=org/apache/sling/api/resource/Resource/CHANGED",
EventConstants.EVENT_TOPIC + "=com/day/cq/replication/ReplicationAction"
}
)
public static class AssetActivityListener implements EventHandler {
private static final Logger log = LoggerFactory.getLogger(AssetActivityListener.class);
@Reference
private ResourceResolverFactory resolverFactory;
@Override
public void handleEvent(Event event) {
String path = (String) event.getProperty("path");
// 👉 Only process DAM assets
if (path == null || !path.startsWith("/content/dam")) {
return;
}
try (ResourceResolver resolver = resolverFactory.getServiceResourceResolver(
Collections.singletonMap(ResourceResolverFactory.SUBSERVICE, "datawriter"))) {
Resource res = resolver.getResource(path);
if (res == null) return;
Asset asset = res.adaptTo(Asset.class);
if (asset == null) return;
Resource metadata = resolver.getResource(asset.getPath() + "/jcr:content/metadata");
if (metadata == null) return;
// Create or reuse custom node under metadata
Resource customTimeline = metadata.getChild("customTimelineData");
if (customTimeline == null) {
customTimeline = resolver.create(metadata, "customTimelineData", Collections.emptyMap());
}
// 🧩 Main Fields: User, Timestamp, Comment, Action Type
String actionType = "Asset Modified";
if (event.getTopic().contains("ReplicationAction")) {
actionType = "Asset Published";
}
String comment = "System auto-logged action for " + actionType;
Map<String, Object> props = new HashMap<>();
props.put("user", resolver.getUserID());
props.put("timestamp", Calendar.getInstance());
props.put("action", actionType);
props.put("comment", comment);
// Create event node
String nodeName = "event-" + System.currentTimeMillis();
resolver.create(customTimeline, nodeName, props);
resolver.commit();
log.info("✅ Added timeline entry: {} by user {}", actionType, resolver.getUserID());
} catch (Exception e) {
log.error("❌ Error creating custom asset timeline entry for {}", path, e);
}
}
}
// ============================================================
// 🧱 PART 2: Custom Servlet — Returns Timeline Data as JSON
// ============================================================
@Component(
service = { Servlet.class },
property = {
"sling.servlet.paths=/bin/custom/assettimeline",
"sling.servlet.methods=GET"
}
)
public static class AssetTimelineServlet extends SlingAllMethodsServlet {
private static final Logger log = LoggerFactory.getLogger(AssetTimelineServlet.class);
@Override
protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response) throws IOException {
String assetPath = request.getParameter("path");
if (assetPath == null || assetPath.isEmpty()) {
response.setStatus(400);
response.getWriter().write("{\"error\": \"Missing 'path' parameter\"}");
return;
}
JSONArray result = new JSONArray();
ResourceResolver resolver = request.getResourceResolver();
try {
Resource assetRes = resolver.getResource(assetPath);
Asset asset = assetRes != null ? assetRes.adaptTo(Asset.class) : null;
if (asset != null) {
Resource timelineData = resolver.getResource(asset.getPath() + "/jcr:content/metadata/customTimelineData");
if (timelineData != null) {
for (Resource eventRes : timelineData.getChildren()) {
ValueMap vm = eventRes.getValueMap();
JSONObject eventJson = new JSONObject();
eventJson.put("user", vm.get("user", ""));
eventJson.put("action", vm.get("action", ""));
eventJson.put("comment", vm.get("comment", ""));
Calendar cal = vm.get("timestamp", Calendar.class);
eventJson.put("timestamp", cal != null ? cal.getTimeInMillis() : System.currentTimeMillis());
result.put(eventJson);
}
}
}
response.setContentType("application/json");
response.getWriter().write(result.toString());
} catch (Exception e) {
log.error("Error retrieving asset timeline data for {}", assetPath, e);
response.setStatus(500);
response.getWriter().write("{\"error\":\"Internal server error\"}");
}
}
}
}
Thanks & regards,
Vishal
Views
Replies
Total Likes
The first step is to capture additional information, such as user ID and comments, during workflows or asset actions. This can be done using a custom workflow step or event listener, allowing you to store this data in the asset's metadata for later reference.
Next, you can decide whether to create a custom servlet that outputs only the data you need or to leverage the out-of-the-box JSON servlet to retrieve the stored data. To do this, you would call the following URL: /path/to/asset/jcr:content/metadata.json, assuming all the required data is stored under the metadata node.
You can also refer to this article on how to add new entries to the timeline. In your case, it will differ because you are looking to include other information rather than the file name, but the overall approach remains valid.
Views
Replies
Total Likes
Views
Likes
Replies
Views
Likes
Replies