AEM Assets can feel magic until the moment a single image brings a publish queue to its knees.
Renditions and workflows are the quiet gears behind that magic, so let’s talk about the choices that keep them smooth.
If you are rolling out AEM for a large brand library or a content heavy site, renditions are your daily bread. They decide file size, image quality, and how fast your pages paint on phones and desktops.
Right next to renditions lives the AEM workflow engine. It gives you a path for what happens on ingest, what runs after the fact, and where to plug your custom bits when the default step is not enough.
What a rendition really is in AEM Assets
A rendition is just another file stored under an asset node. The original sits under jcr:content/renditions/original, and every extra version hangs out beside it. AEM ships with the DAM Update Asset model which creates web friendly sizes, thumbnails, and a few extras like web enabled images. If you link with Dynamic Media Classic you can lean on server side resizing, but many teams still want a fixed set of files ready to go inside the repository.
The big call is generate ahead of time or render on request. Ahead of time is simple to reason about and behaves well with CDN caching. On request saves space but moves the cost to publish time or first view. Most teams pick a split approach. Bake the common sizes during ingest, then add a service for rarely used sizes when authors ask for them.
DAM Update Asset workflow and launchers
Out of the box, a launcher watches for asset create events and kicks off the dam update asset model. The steps include metadata extraction, sub assets for Photoshop and Illustrator, and image transforms. Before you change anything, clone the model to your project space and keep the original untouched. Then prune steps you do not need. Every minute saved here saves you hours when someone drops a ten gig batch at four in the afternoon.
Launchers are configured under /etc/workflow/launcher. Keep them tight. Watch only the folders you need and only the node types that make sense. You can also build a second model for heavy assets and trigger it in a maintenance window with a manual start. That way daytime uploads stay quick while the night job handles hero images and video transcodes.
// Example: custom WorkflowProcess to create a JPEG rendition
package com.example.aem.assets;
import com.day.cq.dam.api.Asset;
import com.day.cq.dam.api.Rendition;
import com.day.cq.dam.commons.util.DamUtil;
import com.day.cq.workflow.exec.WorkItem;
import com.day.cq.workflow.exec.WorkflowProcess;
import com.day.cq.workflow.metadata.MetaDataMap;
import org.apache.commons.io.IOUtils;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Service;
import org.apache.felix.scr.annotations.Property;
import org.osgi.service.component.annotations.Reference;
import org.osgi.framework.Constants;
import javax.jcr.Session;
import java.io.InputStream;
@Component(immediate = true, metatype = true, label = "Custom JPEG Rendition Process")
@Service
public class JpegRenditionProcess implements WorkflowProcess {
@Property(value = "Creates a 1200 wide JPEG rendition")
static final String DESCRIPTION = Constants.SERVICE_DESCRIPTION;
public void execute(WorkItem item, com.day.cq.workflow.WorkflowSession wfSession, MetaDataMap args) {
try {
Session session = wfSession.adaptTo(Session.class);
Asset asset = DamUtil.resolveToAsset(session.getNode(item.getWorkflowData().getPayload().toString()));
if (asset == null) { return; }
Rendition original = asset.getOriginal();
try (InputStream input = original.getStream()) {
// call your image library of choice to scale and convert to JPEG
byte[] jpeg = ImageScaler.toJpegWithMaxWidth(input, 1200, 0.82f);
asset.addRendition("cq5dam.web.1200.1200.jpeg", IOUtils.toInputStream(new String(jpeg), "UTF-8"));
}
} catch (Exception e) {
// log and continue so the model does not stall
}
}
}Tradeoffs that matter in real projects
Storage vs CPU. More renditions mean more gigabytes. Fewer renditions move the weight to processing time. If your authoring tier can scale out and you have fast shared storage, you can lean toward generate on request. If storage is cheap and you want predictable publish times, bake more sizes.
Quality vs speed. Image quality settings are the sneaky culprit. A jump from 0.8 to 1.0 for JPEG can double the file size with little visible gain. Measure your actual pages. Cut waste first, then tweak quality where it shows up for users, like banners and product closeups.
One size fits nobody. A single 2000 wide image for every slot sounds simple but it hurts mobile users and authoring throughput. A sweet spot is usually three to five web sizes plus thumbnails. If you run Dynamic Media Classic, pair that with server side presets for campaigns that need extra slices.
Fail fast. Broken assets can stall a model. Wrap custom steps in try blocks, log errors, and mark a workflow variable so you can report on failures. Nothing is worse than a hidden pile of items in running state when someone is waiting for a banner.
For backfills and cleanups, scripts beat clicking through the console. The Groovy Console is great for one off jobs. Here is a tiny helper to find assets missing a rendition.
// Groovy Console snippet: list assets missing 1200 wide JPEG rendition
import com.day.cq.search.*
import com.day.cq.search.result.*
def qb = resourceResolver.adaptTo(QueryBuilder)
def map = [
"type": "dam:Asset",
"path": "/content/dam",
"property": "jcr:content/renditions/name",
"property.operation": "exists",
"property.value": "cq5dam.web.1200.1200.jpeg",
"property.negate": "true",
"p.limit": "-1"
]
def query = qb.createQuery(PredicateGroup.create(map), session)
def result = query.getResult()
result.hits.each { hit -> println hit.getPath() }Pair that with a custom workflow model you can start on a folder. Drop the assets you want to fix into a temporary folder, run the model, then move them back. This keeps the default launcher calm while you clean house.
Practical setup checklist
- Clone the default model and give it a clear project name. Keep a simple variant for daytime uploads and a heavy variant for batch nights.
- Trim steps you do not need. If you never use PDF thumbnails or full text indexing for a folder, cut them for that path.
- Scope launchers. Point them to the exact folders that need them. Turn off broad rules that watch the entire DAM unless you truly need that.
- Right size renditions. Agree on three to five web sizes. Lock the JPEG quality and stick to it. Document the names so templates and components reference them the same way.
- Monitor and alert. Watch workflow queue depth, average runtime, and failure counts. A simple health check endpoint polled by your monitoring tool goes a long way.
Here is a tiny content rule in Sling to skip heavy processing for small images. It gives you an escape hatch for icons and sprites.
// Pseudo code inside a custom process
if (asset.getOriginal().getSize() < 30 * 1024) {
// small asset, skip heavy steps
return
}And if you need to move the out of the box model for a specific folder, create a launcher with a folder filter and point it to your custom model at /etc/workflow/models/project/update_asset_custom. Keep the rest on the default path so upgrades stay simple.
Renditions and workflows are a balance. Pick the sizes that meet your design, choose when to pay the processing cost, and keep your launchers scoped to the work at hand. The reward is a snappy authoring experience, publish jobs that do not surprise you, and pages that load fast on real devices. That is the kind of quiet win teams feel every day, even if they never see the gears turning.