plugins/aem/cloud-service/skills/code-assessment/asset-manager/SKILL.md
AEM Cloud Service expert skill for the Asset Manager API (com.day.cq.dam.api.AssetManager). Covers two migration paths — Path A (create/upload, replacing createAssetForBinary/getAssetForBinary/client-facing createAsset with Direct Binary Access via @adobe/aem-upload) and Path B (delete, replacing removeAssetForBinary with in-JVM resolver.delete() + commit or external HTTP Assets API with IMS bearer tokens). Covers IMS authentication, service-user setup with crx delete permissions, asset processing pipeline, common pitfalls (hardcoded credentials, AEM calling its own HTTP API), and composition with replication/event-migration skills.
npx skillsauth add adobe/skills asset-managerInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
com.day.cq.dam.api.AssetManager is not removed on AEM CS — but several of its operations are. The binary-path APIs that relied on direct filesystem access (createAssetForBinary, getAssetForBinary, removeAssetForBinary) do not exist on CS because the cloud runtime does not expose a filesystem path to the AEM JVM. Binary I/O for client uploads moves to Direct Binary Access — bytes flow directly between the client and Adobe's binary store, bypassing the JVM entirely.
API status on AEMaaCS:
| API | Status | What to use instead |
|-----|--------|---------------------|
| AssetManager.getAsset(path) | ✅ Supported | No change — still works for reads |
| AssetManager.createAsset(path, InputStream, mimeType, doSave) | ⚠️ Strongly discouraged for client-facing uploads (2 GB binary limit, blocks the JVM, asset-processing pipeline expects Direct Binary Access). Still callable for in-JVM utilities where the binary is small and already in the JVM (bundled resources, fixtures, back-office imports). | Direct Binary Access for client uploads (@adobe/aem-upload JavaScript SDK). In-JVM small-file creation OK. |
| AssetManager.createAssetForBinary(binaryFilePath, doSave) | ❌ Removed — relied on filesystem path the cloud runtime does not expose | Direct Binary Access |
| AssetManager.getAssetForBinary(binaryFilePath) | ❌ Removed | resolver.getResource(repoPath).adaptTo(Asset.class) — look up by repository path, not binary path |
| AssetManager.removeAssetForBinary(binaryFilePath, doSave) | ❌ Removed | In-JVM: resolver.delete(resource) + resolver.commit(). External: HTTP Assets API DELETE /api/assets{path} |
Three CS-specific principles:
| Principle | Why |
|-----------|-----|
| Binary I/O for client uploads goes Direct Binary Access — never through the JVM | The cloud runtime does not have a JVM-accessible filesystem; bytes through the JVM are slow, memory-bound, and blocked by a 2 GB ceiling |
| In-JVM delete uses resolver.delete() + resolver.commit() with a service user — never AEM calling its own HTTP API | Self-looping HTTP adds latency, requires credentials you shouldn't store, and breaks idempotency |
| External callers authenticate with IMS / dev-console bearer tokens — never hardcoded passwords or userId:password strings | AEMaaCS has no admin user with a static password; IMS service credentials are the only supported external auth path |
Uses AssetManager.createAssetForBinary(...) or AssetManager.getAssetForBinary(...) (removed APIs)
→ Apply Path A (C1–C3).
Uses AssetManager.createAsset(path, InputStream, mimeType, doSave) in a client-facing servlet (accepts upload from a browser or external caller)
→ Apply Path A — migrate to Direct Binary Access (C2 client-facing branch).
Uses AssetManager.removeAssetForBinary(...) (removed API)
→ Apply Path B (D1–D3).
Uses both a removed create API AND a removed delete API → Apply Path A first, then Path B. Both transformations are independent.
Uses AssetManager.getAsset(path) only (read access)
→ Already compliant — no migration needed.
Uses AssetManager.createAsset(path, InputStream, mimeType, doSave) in an in-JVM back-office utility (scheduled import, test fixture, asset post-processing — binary is already in the JVM and small)
→ Already supported — verify the surrounding code uses a service-user resolver and closes the stream in try-with-resources, then skip.
One pattern per session. If the file has multiple flows, migrate one direction (create OR delete) at a time.
Before starting: Read ../references/aem-cloud-service-pattern-prerequisites.md and apply SCR→DS, service-user, and SLF4J fixes if present in the same changeset.
Detection is performed by the analyzer (../scripts/analyze.sh), run by the runbook:
bash ../scripts/analyze.sh <workspace-root> --pattern asset-manager
Match criteria (what the detector flags): in a file importing com.day.cq.dam.api.AssetManager, a call to createAssetForBinary, getAssetForBinary, removeAssetForBinary, or createAsset. One finding per call site (each call is individually actionable, unlike the class-level patterns), with the call as the snippet. Parse-level only — gated on the import; the receiver type is not resolved, so an unrelated createAsset(...) in the same file could match (rare).
guided — apply (guided). The analyzer reports each legacy call site; remediation is judgment-based, routed by the call (create/upload → C1–C3, delete → D1–D3) and applied in an apply session.
| Call site | Disposition |
|---|---|
| createAssetForBinary / getAssetForBinary (removed on CS) | apply (guided) → C1–C3 |
| removeAssetForBinary (removed on CS) | apply (guided) → D1–D3 |
| Client-facing createAsset(...) upload | apply (guided) → C1–C3 (Direct Binary Access) |
| In-JVM back-office createAsset(...) with a service-user resolver | skipped: already-compliant |
| Test code (src/test/) | skipped: test-scope |
AssetManager.createAsset)package com.example.servlets;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Reference;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.servlets.SlingAllMethodsServlet;
import com.day.cq.dam.api.Asset;
import com.day.cq.dam.api.AssetManager;
import javax.servlet.ServletException;
import java.io.IOException;
import java.io.InputStream;
@Component(immediate = true, metatype = false)
public class CreateAssetServlet extends SlingAllMethodsServlet {
@Reference
private AssetManager assetManager;
@Override
protected void doPost(SlingHttpServletRequest request, SlingHttpServletResponse response)
throws ServletException, IOException {
String assetPath = request.getParameter("path");
String mimeType = request.getParameter("mimeType");
InputStream inputStream = request.getInputStream();
try {
Asset asset = assetManager.createAsset(assetPath, inputStream, mimeType, true);
response.setContentType("text/plain");
response.getWriter().write("Asset created: " + asset.getPath());
} catch (Exception e) {
System.err.println("Error creating asset: " + e.getMessage());
e.printStackTrace();
response.setStatus(500);
response.getWriter().write("Error: " + e.getMessage());
}
}
}
On AEMaaCS the upload happens directly between the client and the binary store; the AEM JVM orchestrates the upload but does not carry the bytes. Delete the upload-accepting servlet entirely, and replace it with a client-side call to the Direct Binary Access HTTP API via @adobe/aem-upload:
import DirectBinary from '@adobe/aem-upload';
/**
* Uploads a file to AEMaaCS via Direct Binary Access.
* @param {File|Blob} file The binary to upload
* @param {string} assetPath Repository destination, e.g. "/content/dam/my-site/file.pdf"
* @param {string} host AEM author host, e.g. "https://author-p123-e456.adobeaemcloud.com"
* @param {string} token IMS bearer token (Adobe Developer Console service credentials,
* or short-lived user token from the AEM login flow)
*/
async function uploadAsset(file, assetPath, host, token) {
const upload = new DirectBinary.DirectBinaryUpload();
const options = new DirectBinary.DirectBinaryUploadOptions()
.withUrl(`${host}/api/assets${assetPath}`)
.withUploadFiles([{
fileName: file.name,
blob: file,
fileSize: file.size
}])
.withHttpOptions({
headers: {
Authorization: `Bearer ${token}` // IMS / dev-console token — never a static password
}
});
return upload.uploadFiles(options);
}
If the servlet path must remain (routing, ACL, audit reasons), convert it to return 410 Gone with a documented replacement — do not silently call a removed API:
package com.example.servlets;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.servlets.ServletResolverConstants;
import org.apache.sling.api.servlets.SlingAllMethodsServlet;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.Servlet;
import javax.servlet.ServletException;
import java.io.IOException;
@Component(service = Servlet.class, property = {
ServletResolverConstants.SLING_SERVLET_PATHS + "=/bin/createasset"
})
public class CreateAssetServlet extends SlingAllMethodsServlet {
private static final Logger LOG = LoggerFactory.getLogger(CreateAssetServlet.class);
@Override
protected void doPost(SlingHttpServletRequest request, SlingHttpServletResponse response)
throws ServletException, IOException {
LOG.warn("Legacy upload endpoint hit — clients must use Direct Binary Access at /api/assets");
response.setStatus(410); // 410 Gone — the operation moved
response.setContentType("application/json");
response.getWriter().write(
"{\"error\":\"Use Direct Binary Access (POST /api/assets + @adobe/aem-upload) to upload assets.\"}");
}
}
removeAssetForBinary API)package com.example.servlets;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Reference;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.servlets.SlingAllMethodsServlet;
import com.day.cq.dam.api.AssetManager;
import javax.servlet.ServletException;
import java.io.IOException;
@Component(immediate = true, metatype = false)
public class DeleteAssetServlet extends SlingAllMethodsServlet {
@Reference
private AssetManager assetManager;
@Override
protected void doDelete(SlingHttpServletRequest request, SlingHttpServletResponse response)
throws ServletException, IOException {
String binaryFilePath = request.getParameter("path");
try {
boolean isDeleted = assetManager.removeAssetForBinary(binaryFilePath, true);
response.setContentType("text/plain");
if (isDeleted) {
response.getWriter().write("Asset deleted: " + binaryFilePath);
} else {
response.setStatus(404);
response.getWriter().write("Asset not found: " + binaryFilePath);
}
} catch (Exception e) {
System.err.println("Error deleting asset: " + e.getMessage());
e.printStackTrace();
response.setStatus(500);
response.getWriter().write("Error: " + e.getMessage());
}
}
}
Server-side code that already has a trusted ResourceResolver should delete assets directly via the resource API. No HTTP, no credentials, no self-loopback through AEM's own API:
package com.example.servlets;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.resource.LoginException;
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.api.servlets.ServletResolverConstants;
import org.apache.sling.api.servlets.SlingAllMethodsServlet;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.Servlet;
import javax.servlet.ServletException;
import java.io.IOException;
import java.util.Collections;
@Component(service = Servlet.class, property = {
ServletResolverConstants.SLING_SERVLET_PATHS + "=/bin/deleteasset"
})
public class DeleteAssetServlet extends SlingAllMethodsServlet {
private static final Logger LOG = LoggerFactory.getLogger(DeleteAssetServlet.class);
private static final String SUBSERVICE = "asset-admin-service";
@Reference
private ResourceResolverFactory resolverFactory;
@Override
protected void doDelete(SlingHttpServletRequest request, SlingHttpServletResponse response)
throws ServletException, IOException {
String assetPath = request.getParameter("path");
if (assetPath == null || assetPath.isEmpty() || !assetPath.startsWith("/content/dam/")) {
response.setStatus(400);
response.setContentType("application/json");
response.getWriter().write("{\"error\":\"valid /content/dam path parameter required\"}");
return;
}
try (ResourceResolver resolver = resolverFactory.getServiceResourceResolver(
Collections.singletonMap(ResourceResolverFactory.SUBSERVICE, SUBSERVICE))) {
Resource resource = resolver.getResource(assetPath);
if (resource == null) {
response.setStatus(404);
response.setContentType("application/json");
response.getWriter().write("{\"error\":\"asset not found\"}");
return;
}
resolver.delete(resource);
resolver.commit();
response.setContentType("application/json");
response.getWriter().write("{\"success\":true}");
LOG.info("Deleted asset at {}", assetPath);
} catch (LoginException e) {
LOG.error("Could not open service resolver for subservice '{}'", SUBSERVICE, e);
response.setStatus(500);
response.setContentType("application/json");
response.getWriter().write("{\"error\":\"internal error\"}");
} catch (PersistenceException e) {
LOG.error("Commit failed while deleting asset {}", assetPath, e);
response.setStatus(500);
response.setContentType("application/json");
response.getWriter().write("{\"error\":\"internal error\"}");
}
}
}
If the deleter is outside the AEM JVM (integration backend, CLI, worker), call the HTTP API with an IMS or dev-console bearer token — never a hardcoded password:
import axios from 'axios';
/**
* Deletes an asset from AEMaaCS via the HTTP Assets API.
* Caller must provide a bearer token from Adobe Developer Console service credentials
* or a short-lived user token from the AEM login flow.
*/
export async function deleteAsset({ host, assetPath, bearerToken }) {
const response = await axios.delete(`${host}/api/assets${assetPath}`, {
headers: { Authorization: `Bearer ${bearerToken}` },
validateStatus: status => status === 200 || status === 204 || status === 404
});
return response.status !== 404;
}
createAssetForBinary / getAssetForBinaryThese APIs do not exist on AEMaaCS. Remove every call.
// BEFORE (removed APIs)
assetManager.createAssetForBinary(binaryFilePath, doSave);
Asset asset = assetManager.getAssetForBinary(binaryFilePath);
Replacements:
| Legacy call | AEMaaCS replacement |
|-------------|---------------------|
| createAssetForBinary(binaryFilePath, doSave) | Direct Binary Access via @adobe/aem-upload or HTTP POST /api/assets. The binary never sits on the AEM filesystem. |
| getAssetForBinary(binaryFilePath) | resolver.getResource(repoPath).adaptTo(Asset.class) using the repository path (not a filesystem path) |
// Direct Binary Access — client side
const DirectBinary = require('@adobe/aem-upload');
const upload = new DirectBinary.DirectBinaryUpload();
const options = new DirectBinary.DirectBinaryUploadOptions()
.withUrl(`${host}/api/assets${assetPath}`)
.withUploadFiles(uploadFiles)
.withHttpOptions({ headers: { Authorization: `Bearer ${token}` } });
await upload.uploadFiles(options);
// Look up by repository path — replaces getAssetForBinary
Resource resource = resolver.getResource("/content/dam/my-site/report.pdf");
Asset asset = resource != null ? resource.adaptTo(Asset.class) : null;
The legacy "binary path" concept (a filesystem path under the AEM install) does not exist on AEMaaCS. Anywhere your legacy code passed a binary path, replace with the asset's repository path (/content/dam/...).
createAsset(path, InputStream, mimeType, overwrite) needs to changeApply the decision matrix:
| Caller | Keep createAsset(...)? |
|--------|--------------------------|
| Client-facing servlet accepting multipart / InputStream from a browser or external caller | No — migrate to Direct Binary Access |
| Back-office utility creating small assets from a bundled resource or another already-in-JVM stream (fixtures, reports, migration imports) | Yes, still supported — use a service-user resolver and close the stream |
| Scheduled asset ingestion pulling from an external source | Prefer Direct Binary Access through the HTTP API; the scheduler-triggered job acts as an external client |
When keeping createAsset for in-JVM use:
try (ResourceResolver resolver = resolverFactory.getServiceResourceResolver(
Collections.singletonMap(ResourceResolverFactory.SUBSERVICE, "asset-admin-service"));
InputStream stream = bundleContext.getBundle().getResource(bundledPath).openStream()) {
AssetManager assetManager = resolver.adaptTo(AssetManager.class);
Asset asset = assetManager.createAsset(destPath, stream, mimeType, true);
LOG.info("Seeded asset at {}", asset.getPath());
} catch (LoginException e) {
LOG.error("Could not open service resolver for subservice 'asset-admin-service'", e);
} catch (IOException e) {
LOG.error("Failed to read bundled asset {}", bundledPath, e);
}
This is legitimate on AEMaaCS — it does not hit the removed binary APIs and does not accept arbitrary client uploads. Constraints:
USER/PASSWORD maps)InputStream in try-with-resources — leaking streams blocks bundle reloaddoSave=true auto-commits; if you pass false, call resolver.commit() explicitlyRemove (when createAssetForBinary / getAssetForBinary are gone):
import com.day.cq.dam.api.metadata.MetaDataMap; // if only used for deprecated flow
Keep com.day.cq.dam.api.Asset and com.day.cq.dam.api.AssetManager when createAsset / getAsset are still in use.
Remove (SCR → DS):
import org.apache.felix.scr.annotations.*;
Add — servlet shell:
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.servlets.ServletResolverConstants;
import org.apache.sling.api.servlets.SlingAllMethodsServlet;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.Servlet;
Add — in-JVM createAsset utility:
import org.apache.sling.api.resource.LoginException;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.osgi.service.component.annotations.Reference;
import java.io.InputStream;
import java.util.Collections;
removeAssetForBinary with ResourceResolver#delete (in-JVM)// BEFORE (removed API)
boolean isAssetDeleted = assetManager.removeAssetForBinary(binaryFilePath, doSave);
// AFTER — in-JVM with service-user resolver
try (ResourceResolver resolver = resolverFactory.getServiceResourceResolver(
Collections.singletonMap(ResourceResolverFactory.SUBSERVICE, "asset-admin-service"))) {
Resource resource = resolver.getResource(assetPath); // repository path, not binary path
if (resource == null) {
LOG.info("Asset already gone at {}", assetPath);
return; // idempotent — treat "not found" as success, not failure
}
resolver.delete(resource);
resolver.commit();
LOG.info("Deleted asset at {}", assetPath);
} catch (LoginException e) {
LOG.error("Could not open service resolver for subservice 'asset-admin-service'", e);
} catch (PersistenceException e) {
LOG.error("Commit failed while deleting asset {}", assetPath, e);
}
Rules:
assetPath is the repository path (e.g. /content/dam/site/file.jpg). The legacy "binary path" concept does not exist on AEMaaCS.jcr:removeNode and jcr:removeChildNodes on the asset tree (see Repoinit setup below)./api/assets) from inside the JVM — it adds latency, requires credentials the JVM shouldn't store, and breaks idempotency. Use the resource API directly.path.startsWith("/content/dam/")) — a poorly-validated delete endpoint is a destructive vulnerability.For deleters running outside the AEM JVM (integration backends, CLIs, ingestion workers), call the HTTP API:
import axios from 'axios';
export async function deleteAsset({ host, assetPath, bearerToken }) {
const response = await axios.delete(`${host}/api/assets${assetPath}`, {
headers: { Authorization: `Bearer ${bearerToken}` },
validateStatus: status => status === 200 || status === 204 || status === 404
});
return response.status !== 404;
}
Where bearerToken comes from:
Never hardcode usernames and passwords. Never pass ":password" strings or basic auth literals. AEMaaCS has no static admin password.
// WRONG — hardcoded credentials, no admin password exists on AEMaaCS
const response = await axios.delete(url, {
headers: { Authorization: 'Basic ' + Buffer.from('admin:admin').toString('base64') }
});
// RIGHT — IMS bearer token
const response = await axios.delete(url, {
headers: { Authorization: `Bearer ${imsToken}` }
});
Remove (when all AssetManager delete calls are gone):
import com.day.cq.dam.api.AssetManager; // remove if no other AssetManager usage remains in the file
Keep AssetManager if the file still uses AssetManager.getAsset(...) for reads.
Remove (SCR → DS):
import org.apache.felix.scr.annotations.*;
Add — in-JVM delete servlet:
import org.apache.sling.api.resource.LoginException;
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.api.servlets.ServletResolverConstants;
import org.apache.sling.api.servlets.SlingAllMethodsServlet;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.Servlet;
import javax.servlet.ServletException;
import java.io.IOException;
import java.util.Collections;
The in-JVM create/delete operations acquire a resolver via SUBSERVICE. On AEM CS the backing system user must be created via Repoinit and mapped through ServiceUserMapperImpl.amended. The classic UI-based user admin is not available.
Repoinit (in ui.config):
create service user asset-admin-service
set ACL for asset-admin-service
allow jcr:read,rep:write,jcr:removeNode,jcr:removeChildNodes on /content/dam
end
The jcr:removeNode and jcr:removeChildNodes privileges are required for delete. rep:write alone is not sufficient. Without them, resolver.delete(resource) succeeds in memory but resolver.commit() throws AccessDeniedException.
Service-user mapping (ui.config, file named e.g. org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended-asset-admin.cfg.json):
{
"user.mapping": [
"com.example.mybundle:asset-admin-service=[asset-admin-service]"
]
}
Mapping conventions:
ServiceUserMapperImpl config=[service-user-name]userName form: must match the bundle owning the servlet / utilitySee ../references/aem-cloud-service-pattern-prerequisites.md for the full Repoinit workflow.
For new asset code (not migrating legacy), the rules collapse to three patterns:
Reading an asset — use the resource API; do not adapt to AssetManager unnecessarily:
try (ResourceResolver resolver = resolverFactory.getServiceResourceResolver(
Collections.singletonMap(ResourceResolverFactory.SUBSERVICE, "asset-reader-service"))) {
Resource resource = resolver.getResource(assetPath);
if (resource == null) {
return Optional.empty();
}
Asset asset = resource.adaptTo(Asset.class);
return Optional.ofNullable(asset);
} catch (LoginException e) {
LOG.error("Could not open service resolver", e);
return Optional.empty();
}
Uploading from a client — never write an upload-accepting servlet. The client calls Direct Binary Access:
import DirectBinary from '@adobe/aem-upload';
async function uploadAsset(file, assetPath, host, token) {
const upload = new DirectBinary.DirectBinaryUpload();
const options = new DirectBinary.DirectBinaryUploadOptions()
.withUrl(`${host}/api/assets${assetPath}`)
.withUploadFiles([{ fileName: file.name, blob: file, fileSize: file.size }])
.withHttpOptions({ headers: { Authorization: `Bearer ${token}` } });
return upload.uploadFiles(options);
}
Deleting from in-JVM code — service-user resolver + delete() + commit():
try (ResourceResolver resolver = resolverFactory.getServiceResourceResolver(
Collections.singletonMap(ResourceResolverFactory.SUBSERVICE, "asset-admin-service"))) {
Resource resource = resolver.getResource(assetPath);
if (resource == null) {
return; // idempotent — already gone
}
resolver.delete(resource);
resolver.commit();
} catch (LoginException e) {
LOG.error("Could not open service resolver", e);
} catch (PersistenceException e) {
LOG.error("Commit failed for {}", assetPath, e);
}
What this captures:
getResource().adaptTo(Asset.class) for reads — direct AssetManager.adaptTo is rarely neededUSER/PASSWORD mapsTrigger replication after a delete: combine with the replication skill:
// Inside the delete code path, after resolver.commit()
DistributionRequest request = new SimpleDistributionRequest(
DistributionRequestType.DELETE, false, assetPath);
DistributionResponse response = distributor.distribute("publish", resolver, request);
if (!response.isSuccessful()) {
LOG.warn("Delete distribution failed for {}: {}", assetPath, response.getMessage());
}
React to asset add/remove events: use the resource-change-listener skill to observe /content/dam, then a JobConsumer to handle the business logic. Do not put asset processing inside onChange().
Hook into the asset processing pipeline: Adobe-managed asset processing runs out-of-band on AEMaaCS. To act after processing completes (renditions ready, metadata extracted), listen for the relevant DAM workflow completion event via the event-migration skill.
Cross-cutting:
createAssetForBinary(, getAssetForBinary(, or removeAssetForBinary( calls remaingetAdministrativeResourceResolver() or USER/PASSWORD auth maps":password", literal bearer tokens, basic-auth literals)getServiceResourceResolver(SUBSERVICE) used in try-with-resources for any in-JVM AEM operationorg.apache.felix.scr.annotations.* importsSystem.out / System.err / printStackTracePath A (create / upload):
@adobe/aem-upload) — any residual servlet returns 410 Gone with a documented replacementcreateAsset(path, InputStream, mimeType, overwrite) remains only in in-JVM utilities where the binary is small and already in the JVM (fixtures, bundled resources, post-processing)InputStream closed in try-with-resourcescreateAsset flowsgetAssetForBinary — resolver.getResource(repoPath).adaptTo(Asset.class) used instead/content/dam/...)Path B (delete):
resolver.delete(resource) + resolver.commit() — not AEM calling its own HTTP APIresource == null treated as successful no-op (idempotent delete)startsWith("/content/dam/")) before deletePersistenceException caught separately from LoginException and logged with the pathjcr:removeNode and jcr:removeChildNodes (not just rep:write)Configuration:
ui.config with correct ACL privileges for the operation (read for reads, removeNode for deletes)ServiceUserMapperImpl.amended-*.cfg.json maps <bundle>:<subservice> → the service user (principal-name form)mvn clean install succeeds with no SCR-related or deprecated-API warnings| Symptom | Log to search | Fix direction |
|---------|--------------|--------------|
| NoSuchMethodError: AssetManager.createAssetForBinary at runtime | the exception name verbatim | Legacy binary-path API isn't on the CS SDK classpath — complete the C1 migration |
| NoSuchMethodError: AssetManager.removeAssetForBinary at runtime | the exception name verbatim | Same as above — complete the D1 migration |
| Upload endpoint returns 500 or times out under load | client-side socket timeout or payload too large; server-side may show memory pressure | Client is uploading through the JVM via legacy servlet — migrate to Direct Binary Access; the bytes should never flow through AEM's JVM |
| LoginException: Unable to retrieve the service resource resolver | the exception name verbatim | Service user not provisioned via Repoinit, OR ServiceUserMapperImpl.amended-*.cfg.json missing/misnamed, OR bundle symbolic name in mapping doesn't match the bundle owning the servlet |
| AccessDeniedException on resolver.commit() after resolver.delete() | AccessDeniedException near a commit() call | Service user lacks jcr:removeNode and/or jcr:removeChildNodes on the asset tree — extend Repoinit ACL |
| Delete servlet returns 200 but the asset is still present | none | Code path is missing resolver.commit() — delete() only stages the operation; without commit(), try-with-resources closes the resolver and silently discards the pending change |
| External caller gets 401 Unauthorized on /api/assets | server-side 401 or WWW-Authenticate: Bearer | Caller is using basic auth with a hardcoded password — switch to an IMS / dev-console bearer token |
| Direct Binary Access upload fails with CORS error | browser console CORS policy: No 'Access-Control-Allow-Origin' | Client origin not configured in AEMaaCS Cross-Origin Resource Sharing OSGi config — add the origin to the allowlist |
| Direct Binary Access upload completes but processed renditions never appear | none synchronously | Asset processing is asynchronous on AEMaaCS — verify in the DAM admin UI; if stuck, check the asset processing pipeline status in Cloud Manager |
Calling createAssetForBinary / getAssetForBinary / removeAssetForBinary — these APIs do not exist on AEMaaCS. The compile-time signature is gone; if a build succeeds against legacy AEM jars and then deploys to CS, every call throws NoSuchMethodError at runtime.
Accepting client uploads via a Java servlet that calls createAsset(path, InputStream, ...) — funnels every byte through the AEM JVM. Slow, memory-bound, blocked by a 2 GB ceiling, and bypasses the asset processing pipeline's Direct Binary Access path. Migrate to @adobe/aem-upload on the client.
Hardcoding credentials — Authorization: Basic Buffer.from('admin:admin').toString('base64'), literal ":password" strings, plain-text bearer tokens. AEMaaCS has no static admin password; external callers must use IMS / dev-console service credentials that produce short-lived bearer tokens.
AEM calling its own HTTP API from inside the JVM — DELETE /api/assets/... from a Java servlet adds latency, requires credentials the JVM shouldn't hold, and breaks idempotency. Use the resource API directly (resolver.delete() + resolver.commit()).
Forgetting resolver.commit() after resolver.delete() — try-with-resources closes the resolver and silently discards the pending delete. Always commit before the resolver closes.
Treating "asset not found" as an error — delete operations should be idempotent. If the asset is already gone, return success (200/204), not 404 as a failure. Otherwise retries against a now-deleted resource fail unnecessarily.
Service user with only rep:write — rep:write does NOT include node removal. The Repoinit ACL must grant jcr:removeNode and jcr:removeChildNodes for delete operations to succeed.
Confusing repository path with binary path — getAssetForBinary("/var/dam-binaries/foo.jpg") is gone. The new world only has repository paths (/content/dam/site/foo.jpg). Any code path that constructed a "binary path" should be deleted along with the legacy API call.
Not validating the path parameter on a delete endpoint — request.getParameter("path") without validation is a destructive vulnerability. Anchor to /content/dam/ (or your tenant's subtree) before calling resolver.delete().
Catching Exception broadly and swallowing the cause — LoginException, PersistenceException, and IOException carry distinct failure modes. Catch them separately and log with the asset path so audit logs are actionable.
| Need | Use |
|------|-----|
| Read an asset (path lookup, metadata, renditions) | resolver.getResource(path).adaptTo(Asset.class) — AssetManager.getAsset(path) is also supported |
| Upload from a browser or external client | Direct Binary Access via @adobe/aem-upload (HTTP POST /api/assets) |
| Create a small asset from a bundled resource (in-JVM) | AssetManager.createAsset(path, InputStream, mimeType, true) with a service-user resolver — still supported |
| Delete from in-JVM code (servlet, scheduler, JobConsumer) | resolver.delete(resource) + resolver.commit() with a service-user resolver |
| Delete from outside AEM (integration backend, CLI) | HTTP DELETE /api/assets{path} with an IMS / dev-console bearer token |
| Update metadata on an existing asset | resource.adaptTo(ModifiableValueMap.class) on /content/dam/.../jcr:content/metadata + resolver.commit(). For external callers: PUT /api/assets{path}.json |
| React to asset events (added, processed, deleted) | ResourceChangeListener on /content/dam (see resource-change-listener skill) |
| Trigger replication after asset operations | Distributor.distribute(...) (see replication skill) |
| Hook the asset processing pipeline | Asset processing runs out-of-band on AEMaaCS — listen for DAM workflow completion events via event-migration skill, not the legacy dam/update_asset workflow internals |
| Bulk asset import / migration | Direct Binary Access from the migration tool, OR Adobe-managed bulk import (Cloud Manager) — not a JVM loop calling createAsset |
Binary I/O does not flow through the AEM JVM on AEMaaCS. This is the single biggest architectural shift. Direct Binary Access uploads negotiate a short-lived URL between the client and Adobe's binary store; the AEM JVM only handles metadata and the post-upload complete call. Any pattern that involves the JVM holding bytes (large InputStream over a servlet, createAssetForBinary, in-memory image manipulation before upload) belongs to the legacy AEM 6.x architecture and won't perform on CS.
Asset processing is asynchronous and Adobe-managed. When Direct Binary Access completes the upload, the asset is queued for processing — renditions, metadata extraction, smart tags. The completion of the upload does NOT mean renditions are ready. Code that depends on processed renditions must either:
jcr:content/renditions path with backoffAuthentication splits two ways on AEMaaCS. In-JVM code uses a service-user resolver — no credentials, no tokens, just a SUBSERVICE name resolved by ServiceUserMapperImpl.amended. External callers use IMS or dev-console bearer tokens — short-lived, scope-limited, never hardcoded. There is no third option. Hardcoded passwords, basic auth literals, and "admin" accounts do not exist on CS.
Delete operations should be idempotent. Treat resource == null as success; the result of "asset is not present" is the same as "asset was deleted successfully". This matters for retry semantics on JobConsumers and external callers — non-idempotent delete leads to flaky tests and false alarms.
Failure modes to plan for:
NoSuchMethodError at runtime — bundle compiled against legacy AEM jars but deployed to CS. Update bundle dependencies.AccessDeniedException on commit — service-user ACL missing jcr:removeNode for deletes or rep:write for creates. Extend Repoinit.request.getInputStream() + createAsset patterns.The following are practical observations from production AEMaaCS code rather than items documented as part of Adobe's official Asset Manager or DAM API contract. They are useful design guidance but should not be treated as guarantees:
createAsset doSave=true versus explicit commit. Empirically, createAsset(..., true) auto-commits on most CS versions, but behavior across resolver lifecycles (especially when the call is wrapped in a transaction-like sequence with other writes) is not formally documented. If you need deterministic commit ordering with other writes in the same flow, prefer createAsset(..., false) followed by an explicit resolver.commit().@adobe/aem-upload flow uploads the binary but does not set custom metadata. To attach dc:title, dc:description, or custom properties, follow up with a PUT /api/assets{path}.json (external) or a ModifiableValueMap write to jcr:content/metadata (in-JVM, after the asset is processed). Doing both in parallel can race the processing pipeline.tools
Use the run-workflow MCP to discover, compose, execute, publish, and save Adobe Firefly workflows. TRIGGER when: user asks what actions are available, what the MCP can do, how to process images/video/3D via workflow, wants to build/run/save/publish a workflow, OR pastes any workflow/batch/execution ID. BARE ID (UUID/workflowId/batchId) = INSPECT ONLY — call inspect_run, NEVER run_workflow_submit. ALWAYS call list_actions first for capability/discovery questions. DO NOT TRIGGER for direct Firefly API calls without MCP (use firefly-api-specs).
tools
Run predefined featured workflows via run-workflow MCP. TRIGGER when user names a featured workflow (retargeting, banners at scale, localization, packaging, banner advertising, etc.) or asks to run a known marketing/production workflow. Requires run-workflow MCP. ALWAYS call get_featured_workflow before compose_workflow. DO NOT TRIGGER for custom one-off workflows with no named template — use run-workflow skill.
tools
Migrate an Adobe Commerce App Builder project from the Integration Starter Kit or Checkout Starter Kit to the new App Management approach. Run from the root of the App Builder project to be migrated. Pass --auto to skip confirmation prompts (suitable for CI or batch use) — auto mode prints a summary of all Q&A questions answered with their defaults. Pass --doc-scan-only to scan README.md and env.dist for outdated content without modifying any files. Use when the user wants to migrate an App Builder project from the Integration Starter Kit or Checkout Starter Kit to the App Management approach, or mentions upgrading their Adobe Commerce extension architecture.
development
Add or modify webhook interceptors in an Adobe Commerce app. Use when the user wants to intercept Commerce operations to validate input, append data, or modify behavior — before or after execution. Requires a base app initialized with commerce-app-init.