Expand my Community achievements bar.

Submissions are now open for the 2026 Adobe Experience Maker Awards.

AEM 6.5 – Extending Asset Timeline in assetdetails.html to Display Additional Custom Fields (User, Comments, Workflow Actions, etc.)

Avatar

Level 7

Hi Team,

 

I’m currently working on an implementation to enhance the Asset Timeline displayed in AEM’s assetdetails.html view.

tushaar_srivastava_0-1759740185754.png

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:

  • Action Fields to Display in Timeline
  • Asset Submitted User ID, Date, Comment
  • Asset Published User ID, Date, Comment
  • Edit Approved User ID, Date, Comment
  • Dynamic Media Custom Workflow Retriggered User ID, Date, Comment
  • Archived User ID, Date, Comment

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

 

@kautuk_sahni  @arunpatidar @BrianKasingli  @lukasz-m 

Topics

Topics help categorize Community content and increase your ability to discover relevant content.

4 Replies

Avatar

Level 4

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

Avatar

Level 7

Hi @VishalKa5  could you please provide the Java docs for this custom implementation because there is nothing called as AssetTimelineProvider

Avatar

Level 4

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

Avatar

Level 10

hi @tushaar_srivastava

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.