Org Skills

aem-backend-development

Write production-grade Adobe Experience Manager back-end Java for AEM as a Cloud Service (with 6.5 notes) — OSGi Declarative Services, runmode configs, Sling servlets and filters, service users, queries and Oak indexes, jobs, schedulers, workflows and replication.

Download .zip Raw Source
When agents use itUse this whenever the user writes or debugs an OSGi @Component, @Designate/@ObjectClassDefinition config, .cfg.json in ui.config, $[env:] or $[secret:] values, a SlingSafeMethodsServlet or SlingAllMethodsServlet, @SlingServletResourceTypes, a Sling filter, ResourceResolverFactory or service user and repoinit, QueryBuilder or JCR-SQL2, a custom oak:index, JobConsumer, scheduler cron, ResourceChangeListener, EventHandler, WorkflowProcess or launcher, Replicator or content distribution, or asks why code fails on Cloud Service or leaks sessions. Also apply it when reviewing AEM core bundle code or migrating 6.5 code to Cloud Service.

Install

Copilot (VS Code, Visual Studio, Copilot CLI and github.com) reads skills from the repository — commit them so the whole team gets them.

npx skills add AGCO-Global/org-skills --skill aem-backend-development -a github-copilot
# or with the org installer (adds .github/skills/aem-backend-development):
npx -y github:AGCO-Global/org-skills add skill aem-backend-development

Uses the open skills CLI. Works with Claude Code, Codex, Cursor, Copilot, Gemini CLI, OpenCode, Windsurf and 60+ others — it asks which agent to install into.

npx skills add AGCO-Global/org-skills --skill aem-backend-development
# user-level instead of project-level:
npx skills add AGCO-Global/org-skills --skill aem-backend-development -g

Installs the aem-development-skills plugin, which bundles all AEM / Development skills and keeps them updated.

/plugin marketplace add AGCO-Global/org-skills
/plugin install aem-development-skills@org-skills
# or just this skill, in this repository:
npx skills add AGCO-Global/org-skills --skill aem-backend-development -a claude-code

Use Download .zip above, then upload it under Customize → Skills → Upload skill. Team and Enterprise admins can sync this repo as a plugin marketplace instead.

Installs into .agents/skills/, which Codex reads.

npx skills add AGCO-Global/org-skills --skill aem-backend-development -a codex

Installs into Cursor's skills folder.

npx skills add AGCO-Global/org-skills --skill aem-backend-development -a cursor

Installs into Gemini CLI's skills folder.

npx skills add AGCO-Global/org-skills --skill aem-backend-development -a gemini-cli

Any tool with rules, instructions or custom prompts: use Copy SKILL.md above and paste it in. It is plain Markdown.

Commands use your normal git sign-in to GitHub, so they work while the repository is private. Node.js 20+ required.

Skill contents

AEM Back-end Development

AEM back-end code runs as long-lived OSGi singletons inside a horizontally scaled, auto-restarted cluster. Write every service as if it serves concurrent requests on several pods at once, with an immutable /apps, no admin access, and a restart that can happen at any moment — then it works on 6.5 too.

1. Cloud Service vs 6.5 — know which rules apply

Topic AEM as a Cloud Service AEM 6.5 (on-prem / AMS)
/apps, /libs Immutable at runtime; only deployable via ui.apps Writable (but treat as immutable anyway)
Runmodes Fixed: author/publish × dev/stage/prod (+ rde) Arbitrary custom runmodes
Config values $[env:NAME;default=x], $[secret:NAME] via Cloud Manager Per-environment files or sling:OsgiConfig nodes
Publish tier Independent, disposable instances; no cluster, no writes that must survive Persistent publish instances
Replication Sling Content Distribution via an Adobe pipeline; agents not configurable Replication agents you configure
Logs logs/error.log only; levels per environment Custom log files allowed
Indexes Named <name>-custom-<n>, deployed in code Free naming; reindex manually

2. OSGi Declarative Services

@Component(service = PriceService.class)
@Designate(ocd = PriceServiceImpl.Config.class)
public class PriceServiceImpl implements PriceService {
    @ObjectClassDefinition(name = "Acme – Price Service")
    public @interface Config {
        @AttributeDefinition(name = "Endpoint") String endpoint() default "https://api.example.com";
        @AttributeDefinition(name = "Timeout (ms)") int timeout() default 3000;
    }
    private volatile Config config;                    // swapped atomically on @Modified

    @Reference private HttpClientFactory httpClientFactory;     // static, mandatory (default)

    @Reference(cardinality = ReferenceCardinality.MULTIPLE,
               policy = ReferencePolicy.DYNAMIC, policyOption = ReferencePolicyOption.GREEDY)
    private volatile List<PriceRule> rules;            // replaced as a whole when services come/go

    @Activate @Modified
    protected void activate(Config config) { this.config = config; }
}
  • Program to interfaces, service = Interface.class; keep the impl in an impl package that is not exported.
  • Static reluctant references (default) restart your component when the dependency changes — simple and safe. Use dynamic + volatile only for optional or multiple references that come and go.
  • @Modified lets config change without deactivation; without it, a config change restarts the component and all dependents.
  • Service ranking (Constants.SERVICE_RANKING) decides which impl wins for a unary reference; use it to override, never rely on bundle start order.
  • Thread safety: components are singletons. No request state in fields; ResourceResolver, Session and SimpleDateFormat are not thread-safe; immutable config objects in volatile fields.

3. OSGi configuration per runmode (ui.config)

ui.config/src/main/content/jcr_root/apps/acme/osgiconfig/
  config/                com.acme.core.impl.PriceServiceImpl.cfg.json
  config.author/         …
  config.publish.prod/   …
  config/                org.apache.sling.commons.log.LogManager.factory.config~acme.cfg.json
{ "endpoint": "$[env:PRICE_ENDPOINT;default=https://api.example.com]",
  "apiKey": "$[secret:PRICE_API_KEY]",
  "timeout:Integer": 3000 }
  • Factory configs use PID~name.cfg.json; typed values use "key:Integer" style only where the type isn't inferable from JSON.
  • Most specific runmode folder wins; don't duplicate a full config into five folders — put defaults in config/, override only what differs, or use one config with env vars.
  • Secrets never in Git: $[secret:] on Cloud Service, a vault-fed mechanism on 6.5.

4. Sling servlets and filters

Choice Default Change when…
Binding @SlingServletResourceTypes (+ selectors/extension) Almost never path-bound: @SlingServletPaths bypasses repository ACLs, can't be secured per resource, must be allow-listed in the servlet resolver's execution paths, and is unreachable through Dispatcher without extra rules
Base class SlingSafeMethodsServlet (GET/HEAD) SlingAllMethodsServlet only for state-changing POST/PUT/DELETE
Output JSON with a specific selector + extension (.prices.json) Selectors make responses cacheable at Dispatcher/CDN
@Component(service = Servlet.class)
@SlingServletResourceTypes(resourceTypes = "acme/components/product",
        methods = HttpConstants.METHOD_GET, selectors = "prices", extensions = "json")
public class ProductPricesServlet extends SlingSafeMethodsServlet { … }
  • CSRF: mutating requests go through the Granite CSRF filter; front-end sends the token from /libs/granite/csrf/token.json. Never add your path to CSRF exclusions to "fix" a 403 — send the token.
  • Validate selectors/suffix/parameters; return proper status codes (400/404/405), never a 200 with an error body.
  • Filters: @SlingServletFilter(scope = SlingServletFilterScope.REQUEST, resourceTypes = …, pattern = …) with an explicit service.ranking. Keep them cheap and narrowly scoped — they run on every matching request, including cache-miss floods.

5. ResourceResolver lifecycle and service users

  • Request code uses request.getResourceResolver() — never close it.
  • Background code uses a service user: repoinit creates the user and ACLs, ServiceUserMapperImpl.amended~acme.cfg.json maps com.acme.core:price-sync=[acme-price-sync].
create service user acme-price-sync with path system/acme
set ACL for acme-price-sync
    allow jcr:read,rep:write on /content/acme
end
try (ResourceResolver rr = resolverFactory.getServiceResourceResolver(
        Map.of(ResourceResolverFactory.SUBSERVICE, "price-sync"))) {
    // work, then rr.commit()
} catch (LoginException e) { LOG.error("Service user mapping missing for price-sync", e); }
  • Least privilege per sub-service; one sub-service per concern, not one "acme-admin" for everything.
  • loginAdministrative / getAdministrativeResourceResolver are deprecated and blocked on Cloud Service. Never store a resolver in a field.
  • Prefer the Sling Resource API (Resource, ValueMap, ModifiableValueMap, adaptTo); drop to JCR (Node, Session) only for versioning, locking, node types or ACL work.

6. Queries and Oak indexes

  • First ask: can I navigate instead of query? Known paths → getChild/listChildren. Queries are for unbounded search.
  • JCR-SQL2 with ISDESCENDANTNODE, a node type, and property constraints is explicit and easy to index; QueryBuilder is fine for author UIs and predicates but set p.limit, p.guessTotal=true and avoid p.hits=full.
  • Every production query must hit an index — check with the Explain Query tool; a traversal warning is a bug, not a log nuisance.
  • Cloud Service indexes live in code (ui.apps, /oak:index): new ones as acme.productLucene-1-custom-1, extensions of product indexes as e.g. damAssetLucene-<v>-custom-1; bump the suffix to change it (indexes are immutable once deployed). Include only the properties you query/sort on, with tags/selectionPolicy where supported so your index isn't picked up by unrelated queries.

7. Background work: Jobs, schedulers, events, workflows

Need Use Why
Guaranteed, retryable, once-per-cluster work Sling Job (JobManager.addJob, JobConsumer) Persisted, retried, distributed; survives restarts
Periodic task Scheduled Sling Job (createJob(topic).schedule().cron(…).add()) Runs once per cluster; plain Scheduler with scheduler.runOn=LEADER depends on topology and runs on every publish pod
React to content changes ResourceChangeListener (paths + change types) → enqueue a Job Listener must return in milliseconds
React to OSGi events (e.g. replication) EventHandler on specific topics → enqueue a Job Same; never do I/O in the handler
Human steps, approvals, audit trail, author-visible progress AEM Workflow (WorkflowProcess, launcher) Heavyweight: persisted instances, purge needed; use transient workflows when no history is needed
  • Job consumers return JobResult.OK / FAILED (retry) / CANCEL (give up) — make them idempotent.
  • Cloud Service: no long-running or stateful work on publish (instances are recycled); asset renditions belong to asset microservices, post-processing workflows only for custom steps.
  • Scheduler jobs: scheduler.concurrent=false; never hold a resolver across runs.

8. Replication and content mutability

  • Activate via Replicator.replicate(session, ReplicationActionType.ACTIVATE, path, options) or the Sling Distribution API; on Cloud Service this rides Sling Content Distribution and supports the preview tier via an agent filter.
  • Mutable areas: /content, /conf, /var, /home; /oak:index is special — deployed from code, never edited at runtime. Code never writes to /apps at runtime; content packages for ui.content must not contain /apps.
  • Writes on Cloud Service publish are local to one pod and lost on recycle — user data goes to an external store or back to author via a sanctioned flow.

9. Logging and error handling

  • SLF4J with parameterized messages (LOG.debug("Synced {} products", count)); no string concatenation, no e.printStackTrace().
  • Log config via LogManager.factory.config~acme.cfg.json targeting logs/error.log; DEBUG only in dev.
  • Catch specific exceptions (PersistenceException, LoginException, RepositoryException); log once at the boundary with context (path, job id), not at every layer.
  • rr.revert() on failure before rethrowing; commit in batches (e.g. every 500 changes) for bulk writes to keep the session small.

Deliverable format

  1. Target: Cloud Service or 6.5, runmodes affected.
  2. Design: components, their interfaces and references, and which background mechanism was chosen (with the reason).
  3. Code: Java classes, .cfg.json per runmode folder, repoinit and service-user mapping, index definition if a query was added.
  4. Security: service user privileges, servlet binding and CSRF handling, Dispatcher filter/cache rules needed.
  5. Tests: AEM Mocks unit tests for services/servlets; note what needs an integration test.
  6. Ops notes: env vars/secrets to create in Cloud Manager, log categories, reindex impact.

Anti-patterns to reject

  • Admin sessions or loginAdministrative; resolvers stored in fields or never closed.
  • Path-bound servlets under /bin for new features; CSRF exclusions to silence errors.
  • Request or user state in OSGi component fields; non-volatile dynamic references.
  • Queries without an index, p.limit=-1 on user-facing paths, traversal warnings ignored.
  • Long work inside EventHandler, ResourceChangeListener or filters; Scheduler jobs that must run exactly once.
  • Workflows used as a job queue; unpurged workflow instances.
  • Writing to /apps at runtime; relying on publish-side persisted writes on Cloud Service.
  • Hardcoded endpoints, credentials or environment names in Java; secrets in .cfg.json.
  • Custom log files on Cloud Service; System.out; swallowed exceptions.