Using AI Tools for AEM Development
Using AI Tools for AEM Development: Code Faster with Copilot & Claude
Introduction
AEM development has always required deep expertise — Sling models, OSGi configurations, HTL templates, Oak queries, and dispatcher rules all have their own nuances. AI coding assistants like GitHub Copilot, Claude, and ChatGPT now understand AEM well enough to dramatically speed up day-to-day development tasks.
In this post, we'll look at practical examples of how to use AI tools effectively for AEM development — including the right prompts that produce high-quality AEM code.
1. Writing Sling Models with AI
Prompt that works well:
Write an AEM Sling Model for a component called "HeroComponent".
It should:
- Be adaptable from Resource
- Inject title, description, ctaLabel, ctaLink from the component's JCR properties
- Use @PostConstruct to build a full link with a .html extension
- Include @Optional annotations where appropriate
- Follow AEM best practices with proper null checks
AI-Generated Output (Claude / Copilot):
package com.yourproject.core.models;
import com.day.cq.wcm.api.Page;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.models.annotations.Default;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.Optional;
import org.apache.sling.models.annotations.injectorspecific.ScriptVariable;
import org.apache.sling.models.annotations.injectorspecific.ValueMapValue;
import javax.annotation.PostConstruct;
@Model(
adaptables = { SlingHttpServletRequest.class, Resource.class },
adapters = HeroComponent.class,
resourceType = "your-project/components/hero"
)
public class HeroComponentImpl implements HeroComponent {
@ValueMapValue
@Optional
private String title;
@ValueMapValue
@Optional
private String description;
@ValueMapValue
@Optional
private String ctaLabel;
@ValueMapValue
@Optional
private String ctaLink;
@ScriptVariable
@Optional
private Page currentPage;
private String resolvedCtaLink;
@PostConstruct
protected void init() {
if (ctaLink != null && !ctaLink.isEmpty()) {
// Append .html for internal AEM links only
resolvedCtaLink = ctaLink.startsWith("/content")
? ctaLink + ".html"
: ctaLink;
}
}
@Override
public String getTitle() { return title; }
@Override
public String getDescription() { return description; }
@Override
public String getCtaLabel() { return ctaLabel != null ? ctaLabel : "Learn More"; }
@Override
public String getCtaLink() { return resolvedCtaLink; }
}
2. Generating OSGi Configurations with AI
Prompt:
Generate an AEM OSGi configuration class for a scheduler that:
- Runs every 30 minutes
- Calls a method cleanupExpiredSessions()
- Can be enabled/disabled via OSGi config
- Is properly annotated for AEM as a Cloud Service
AI Output:
@Component(service = Runnable.class, immediate = true)
@Designate(ocd = SessionCleanupScheduler.Config.class)
public class SessionCleanupScheduler implements Runnable {
private static final Logger log =
LoggerFactory.getLogger(SessionCleanupScheduler.class);
@ObjectClassDefinition(name = "Session Cleanup Scheduler")
public @interface Config {
@AttributeDefinition(name = "Enabled", description = "Enable/disable scheduler")
boolean enabled() default true;
@AttributeDefinition(
name = "Cron Expression",
description = "Scheduler expression (default: every 30 min)"
)
String scheduler_expression() default "0 0/30 * * * ?";
}
private boolean enabled;
@Activate
protected void activate(Config config,
BundleContext bundleContext,
Map<String, Object> properties) {
this.enabled = config.enabled();
if (enabled) {
Scheduler scheduler = // inject via @Reference
ScheduleOptions opts = scheduler.EXPR(config.scheduler_expression());
opts.name("SessionCleanupJob");
opts.canRunConcurrently(false);
scheduler.schedule(this, opts);
}
}
@Override
public void run() {
if (!enabled) return;
log.info("Running session cleanup...");
cleanupExpiredSessions();
}
private void cleanupExpiredSessions() {
// Your cleanup logic here
log.info("Expired sessions cleaned up.");
}
}
3. Debugging AEM Log Files with AI
Paste an AEM error log into Claude or ChatGPT with this prompt:
Prompt:
I'm getting this error in my AEM logs. I'm running AEM as a Cloud Service.
Explain what's causing it and give me the exact fix:
[ERROR] com.day.cq.wcm.core.impl.page.PageImpl Error while getting
page properties for /content/mysite/en
org.apache.sling.api.resource.LoginException: Cannot login:
javax.jcr.LoginException: Impersonation not allowed for system user
What AI Returns:
A clear explanation: the service user doesn't have the correct impersonation permissions, along with the exact repoinit script to fix it:
create service user my-service-user
set ACL for my-service-user
allow jcr:read on /content
allow rep:impersonateWith on /home/users/system/my-service-user
end
4. Writing Oak Queries with AI
Oak SQL-2 queries are notoriously tricky. AI can write and optimize them instantly.
Prompt:
Write an AEM Oak SQL-2 query to find all cq:Page nodes under
/content/mysite that have a property "brand" equal to "nike"
and were last modified in the last 7 days.
Also show me the Java code to execute it with QueryManager.
AI Output:
SELECT * FROM [cq:Page] AS page
WHERE ISDESCENDANTNODE(page, '/content/mysite')
AND page.[jcr:content/brand] = 'nike'
AND page.[jcr:content/cq:lastModified] > CAST('2026-04-02T00:00:00.000Z' AS DATE)
@Reference
private ResourceResolverFactory resolverFactory;
public List<String> findBrandPages() throws RepositoryException, LoginException {
List<String> results = new ArrayList<>();
Map<String, Object> param = new HashMap<>();
param.put(ResourceResolverFactory.SUBSERVICE, "queryService");
try (ResourceResolver resolver =
resolverFactory.getServiceResourceResolver(param)) {
Session session = resolver.adaptTo(Session.class);
QueryManager qm = session.getWorkspace().getQueryManager();
String sql = "SELECT * FROM [cq:Page] AS page "
+ "WHERE ISDESCENDANTNODE(page, '/content/mysite') "
+ "AND page.[jcr:content/brand] = 'nike' "
+ "AND page.[jcr:content/cq:lastModified] > "
+ "CAST('" + getSevenDaysAgo() + "' AS DATE)";
Query query = qm.createQuery(sql, Query.JCR_SQL2);
query.setLimit(100); // Always limit Oak queries!
NodeIterator it = query.execute().getNodes();
while (it.hasNext()) {
results.add(it.nextNode().getPath());
}
}
return results;
}
5. Writing Unit Tests with AI
Prompt:
Write a JUnit 5 test for the HeroComponentImpl Sling Model above.
Use AEM Mocks (io.wcm.testing.mock.aem). Test:
1. All properties are correctly injected
2. CTA link gets .html appended for /content paths
3. Default CTA label is "Learn More" when not set
AI Output:
@ExtendWith(AemContextExtension.class)
class HeroComponentImplTest {
private final AemContext ctx = new AemContext();
@BeforeEach
void setUp() {
ctx.addModelsForClasses(HeroComponentImpl.class);
}
@Test
void testAllPropertiesInjected() {
ctx.currentResource(ctx.create().resource(
"/content/test/hero",
"sling:resourceType", "your-project/components/hero",
"title", "Welcome to AEM",
"description", "Your content platform",
"ctaLabel", "Get Started",
"ctaLink", "/content/mysite/en"
));
HeroComponent model = ctx.request().adaptTo(HeroComponent.class);
assertNotNull(model);
assertEquals("Welcome to AEM", model.getTitle());
assertEquals("Your content platform", model.getDescription());
assertEquals("Get Started", model.getCtaLabel());
assertEquals("/content/mysite/en.html", model.getCtaLink());
}
@Test
void testDefaultCtaLabel() {
ctx.currentResource(ctx.create().resource(
"/content/test/hero2",
"sling:resourceType", "your-project/components/hero",
"title", "Test"
));
HeroComponent model = ctx.request().adaptTo(HeroComponent.class);
assertEquals("Learn More", model.getCtaLabel());
}
@Test
void testExternalLinkNotModified() {
ctx.currentResource(ctx.create().resource(
"/content/test/hero3",
"sling:resourceType", "your-project/components/hero",
"ctaLink", "https://external.com/page"
));
HeroComponent model = ctx.request().adaptTo(HeroComponent.class);
assertEquals("https://external.com/page", model.getCtaLink());
}
}
6. Best Prompt Patterns for AEM Development
| Task | Prompt Pattern |
|---|---|
| Sling Model | "Write a Sling Model for [component] that injects [fields] and does [logic]. Follow AEM best practices." |
| OSGi Config | "Generate an OSGi config for [service]. It should support [feature] and be compatible with AEMaaCS." |
| HTL Template | "Write an HTL (Sightly) template for [component] using the Sling Model [ModelName]. Include proper data-sly-use and null checks." |
| Dispatcher Rule | "Write an AEM Dispatcher filter rule to [requirement]. Use dispatcher.any syntax." |
| Debug Error | "Explain this AEM error and give the fix: [paste stack trace]" |
| Oak Query | "Write an Oak SQL-2 query to [find X] under [path]. Include Java code to execute it with QueryManager." |
| Unit Test | "Write JUnit 5 tests for [class] using AEM Mocks. Test [scenario 1], [scenario 2]." |
Key Takeaways
- AI tools are most effective for AEM when you provide context — mention AEMaaCS vs AEM 6.x, the component type, and expected behavior.
- Always review AI-generated Oak queries — add limits and check that properties are indexed to avoid full repository traversal.
- Use AI to generate test scaffolding — it's one of the biggest time-savers in AEM development.
- Keep a prompt library for your team — reusable prompts for common AEM tasks dramatically improve consistency.
Comments
Post a Comment