Inspiration
I work daily with AEM as a Full Stack Developer and one problem never goes away — stale content. Pages created years ago sit untouched across large AEM instances with outdated pricing, expired campaigns, old team bios and references to methodologies from 2021. There is no systematic way to find them all, let alone fix them. Content editors don't have time to audit hundreds of pages manually. I wanted to build something that could do this autonomously — and the Alibaba Cloud Model Studio hackathon was the perfect forcing function.
What it does
The Stale Content Refresh Agent is an autonomous AEM content governance agent that:
- Scans the AEM JCR using QueryBuilder to find pages not modified within a configurable threshold
- Extracts the full page JSON via Sling REST API — including text components, accordion items, titles and descriptions
- Reasons about why each page is stale using Qwen-Plus (Alibaba Cloud Model Studio)
- Generates refreshed content using Qwen-Max (Alibaba Cloud Model Studio) — returning a modified JCR tree with updated content in place
- Routes proposed changes through a Streamlit human-in-the-loop approval UI where reviewers can approve, edit, or reject each change
- Writes back approved content directly to the correct JCR component nodes via Sling POST
- Logs every action to SQLite with full audit trail and one-click rollback
No content touches production without human approval. No field mappings are hardcoded — the model handles all page structures dynamically regardless of component type.
How we built it
The pipeline has 5 stages:
1. Scanner — QueryBuilder API filters pages by cq:lastModified using an absolute ISO 8601 timestamp calculated from STALE_THRESHOLD_DAYS
2. Extractor — Sling GET .infinity.json fetches the full JCR tree including nested component nodes
3. Reasoner — Qwen-Plus receives the raw JCR tree and returns structured JSON:
{
"staleness_reason": "References 2021 methodology and expired pricing",
"fields_to_update": ["jcr:content/jcr:title", "jcr:content/root/..."],
"refresh_direction": "Adopt forward-looking tone, remove year references"
}
4. Generator — Qwen-Max receives the original JCR tree + reasoning and returns a modified copy of the same JSON with refreshed content in place
5. Writer — Diffs original and modified JCR trees, POSTs only changed content properties to their exact node paths via Sling POST
The backend is FastAPI + Uvicorn. The approval UI is Streamlit. The audit trail is SQLite. Both Qwen models are called via Alibaba Cloud Model Studio using the OpenAI-compatible endpoint.
Challenges we ran into
AEM QueryBuilder date format — The relative date syntax -90d didn't work on our AEM instance. It required absolute ISO 8601 timestamps calculated dynamically from the current local time, not UTC.
JCR write-back — Early attempts wrote flat properties to jcr:content which had no visible effect on the page. The real fix was passing the full raw JCR tree to the model and letting it return a modified version — then diffing and writing to the exact component node paths.
Namespaced properties — Using agent:bodyCopy caused AEM to return 422 because the agent namespace wasn't registered in JCR. Switched to unnamespaced properties.
Model JSON parsing — Qwen models occasionally wrap JSON responses in markdown code fences. Added .removeprefix("`json") stripping before parsing.
Streamlit re-render flickering — st.rerun() after scan wiped success messages before they were visible. Fixed by storing scan results in st.session_state so they persist across rerenders.
Accomplishments that we're proud of
- Zero hardcoded field mappings — the model reads the full JCR tree and decides what to change. Works on text components, accordion items, titles, descriptions — any structure
- Real JCR write-back — content actually changes on the AEM page, not just metadata
- One-click rollback — original content restored directly in JCR from SQLite backup
- Full audit trail — every approve, edit and reject logged with timestamp and reviewer action
- 14 unit tests passing — scanner, extractor, reasoner, generator, writer and DB all covered
- Production-ready pipeline — error handling, retry logic, pagination, budget tracking all in place
What we learned
- Passing the raw JCR tree directly to the LLM and asking it to return a modified version is far more powerful than trying to parse and map fields manually. The model understands AEM structure better than any regex.
- Qwen-Plus and Qwen-Max have different strengths — Qwen-Plus is better for structured analytical reasoning, Qwen-Max produces higher quality natural language output. Using them in a chain gives you both.
- Human-in-the-loop is not optional for enterprise CMS — content that goes live without review is a liability. The approval UI is not a nice-to-have, it is the product.
- AEM's Sling POST servlet is incredibly powerful for programmatic content updates — any property on any node can be updated with a simple HTTP POST.
What's next for aem-ai-content-agent
- ECS deployment — move the FastAPI backend to Alibaba Cloud ECS for a fully cloud-hosted demo
- Scheduled scans — run the scanner on a cron job so stale pages are caught automatically without manual triggering
- Multi-site support — scan across multiple AEM sites and content roots in a single run
- Image refresh — detect outdated images using vision models and suggest replacements
- Slack notifications — alert content editors when new stale pages are found and approvals are waiting
- AEM workflow integration — trigger the agent from within AEM's native workflow engine instead of a standalone UI
- Content scoring — assign a staleness score to each page so editors can prioritize which pages to refresh first
Log in or sign up for Devpost to join the conversation.