If you remember only one thing from this guide, let it be this: The AEM Groovy Console is the most powerful operational tool in an AEM developer's arsenal, transforming hours of tedious manual repository manipulation or compiled Java deployment cycles into seconds of executing a dynamic script. Most teams get this wrong because they rely on slow Java deployments for one-off data migrations or complex queries that the standard JCR SQL2 cannot handle. Writing custom Java OSGi services or servlets for one-time content manipulation is an anti-pattern. You need a fast, reliable, and expressive way to execute code within the AEM runtime environment, directly interacting with the JCR, Sling, and AEM APIs without server restarts or bundle deployments.
In this exhaustive guide, we will dive deep into everything you need to know about the AEM Groovy Console. We will cover installation, its compatibility with AEM as a Cloud Service (AEMaaCS), how to strictly secure it in production to prevent catastrophic errors, the implicit variables available for immediate use, and how to harness the Fast Action Manager (FAM) to perform bulk updates across millions of nodes without timing out. Finally, I have compiled a comprehensive Script Library featuring ten production-tested Groovy scripts that you can copy, paste, and run today to solve the most common enterprise AEM challenges.
Before diving in, make sure your core AEM backend knowledge is solid. You may want to brush up on related topics in my AEM Backend Development Guide, understand how data is stored with the JCR & Oak Repository Guide, or review how services are wired in the OSGi in AEM Guide.
Why Use the AEM Groovy Console?
Enterprise AEM environments are massive. When a business requirement dictates that 100,000 pages need a specific property updated, or all legacy tags under a specific namespace must be renamed, you have a few options:
- Manual Update: Impossible at scale.
- JCR SQL2 Query: Great for finding nodes, but cannot update them.
- One-off Java Servlet / OSGi Job: Requires writing the code, writing unit tests, getting PR approvals, running the CI/CD pipeline, deploying to non-prod, testing, and deploying to production. This process can take days for a simple update.
- AEM Groovy Console: Write a script, test it on staging, and run it directly in the browser on production in minutes.
The AEM Groovy Console allows you to execute Groovy scripts directly in the AEM environment. Because Groovy is a JVM language that compiles down to Java bytecode, it has seamless, native interoperability with all AEM, Sling, and JCR APIs. You can invoke any OSGi service, adapt resources, and commit sessions exactly as you would in compiled Java, but with the rapid iteration speed of a scripting language.
By eliminating the deployment lifecycle for administrative, operational, and data migration tasks, the Groovy Console drastically reduces Time-To-Resolution (TTR) for critical content issues.
Installation and Cloud Service Compatibility
The Groovy Console is not an out-of-the-box AEM feature. It is a wildly popular open-source project originally created by Citytech (now ICF Next) and now maintained by the AEM community.
Installing on AEM 6.5 (On-Premise / AMS)
For traditional AEM 6.5 environments, installation is straightforward. You simply download the aem-groovy-console-all.zip package from the official GitHub releases page and install it via the AEM Package Manager.
Typically, you embed it within your project's ui.apps or all package so it is automatically deployed to all environments.
<!-- In your all package pom.xml -->
<dependency>
<groupId>be.ida-mediafoundry.aem</groupId>
<artifactId>aem-groovy-console-all</artifactId>
<version>19.0.3</version>
<type>zip</type>
</dependency>AEM as a Cloud Service (AEMaaCS) Compatibility
AEM as a Cloud Service completely shifted the paradigm. In AEMaaCS, the production environment is locked down. You do not have access to the CRXDE Lite, Package Manager, or the traditional OSGi Web Console. Therefore, running a web-based REPL (Read-Eval-Print Loop) like the Groovy Console on a production author tier is inherently problematic in the Cloud Service model.
Does it work in AEMaaCS? Yes, but with strict caveats. The Groovy Console can be installed and run on AEMaaCS Author instances. However, Adobe strongly discourages running arbitrary, untested code on Cloud Service production environments. A poorly written Groovy script that traverses the entire repository synchronously will spike CPU usage, consume all memory, and cause the cloud pods to crash or restart, disrupting authoring activities.
If you deploy the Groovy Console to AEMaaCS:
- Restrict Access: It must be locked down to the
administratorsgroup. - Use FAM: Any script touching more than a few thousand nodes MUST use the Fast Action Manager (FAM) to offload the work to background threads and avoid request timeouts (AEMaaCS has a strict 60-second timeout for requests).
- Local/Dev First: Always test scripts on local Cloud SDK instances or the Dev environment before executing on Staging or Prod.
Security: Locking Down the Console
Leaving the Groovy Console exposed and unsecured is a massive security vulnerability. Any user with access to the console can execute arbitrary Java code, meaning they could theoretically delete the entire repository, read sensitive OSGi configurations, or escalate privileges.
OSGi Configuration for Access Control
By default, the Groovy Console restricts execution to users in the administrators group. You can (and should) configure this via OSGi to explicitly define which groups are allowed to run scripts.
Create an OSGi configuration file in your project:
/ui.config/src/main/content/jcr_root/apps/myproject/osgiconfig/config.author/be.ida_mediafoundry.aem.groovy.console.configuration.impl.ConfigurationServiceImpl.cfg.json
{
"email.enabled": false,
"execution.allowed.groups": [
"administrators",
"system-administrators",
"deployment-managers"
],
"vanity.path.enabled": false,
"audit.disabled": false,
"display.mode": "DARK"
}Key Security Best Practices:
- Never allow authors: Do not grant access to standard content authors or even power authors.
- Enable Auditing: Ensure
audit.disabledisfalse. The console logs every script execution to/var/audit/groovyconsole, so you have a trail of who ran what, and when. - Disable in Publish: The Groovy Console should absolutely never be deployed to the Publish tier. Ensure the dependency is only included in your
authorrunmode configuration. For more on security, see my AEM Security & ACLs Guide.
The Implicit Variables Binding
One of the reasons the Groovy Console is so fast to write for is its "binding". When your script executes, the console injects several critical AEM and Sling objects directly into the script's scope. You don't need to write boilerplate code to obtain a ResourceResolver or a Session.
Here are the most important implicit variables available in every script:
| Variable | Type | Description |
|---|---|---|
session | javax.jcr.Session | The JCR Session of the user executing the script. Use for low-level JCR operations. |
resourceResolver | org.apache.sling.api.resource.ResourceResolver | The Sling Resource Resolver. Use for adapting to models or standard resource manipulation. |
pageManager | com.day.cq.wcm.api.PageManager | The AEM Page Manager for creating, moving, and activating pages. |
queryBuilder | com.day.cq.search.QueryBuilder | The AEM Query Builder service for executing JCR searches. |
nodeBuilder | com.day.cq.search.QueryBuilder | Groovy's NodeBuilder for rapidly creating JCR node structures natively. |
log | org.slf4j.Logger | SLF4J logger instance for writing to error.log. |
out | java.io.PrintWriter | The output stream printed directly to the console UI. |
Because Groovy supports dynamic typing and closures, you can chain these seamlessly.
// Example: Using implicit variables to find a page and print its title
def page = pageManager.getPage("/content/myproject/us/en")
out.println("Page Title: " + page.getTitle())
// Adapting the resource using the resolver
def resource = resourceResolver.getResource("/content/myproject/us/en/jcr:content")
def properties = resource.getValueMap()
out.println("Template: " + properties.get("cq:template", String.class))Interacting with the Fast Action Manager (FAM)
The standard way of iterating through nodes in a Groovy script is synchronous. If you query 50,000 nodes and iterate through them in a for loop, updating and saving each one, the HTTP request executing the script will take a long time. In AEMaaCS, the request will timeout after 60 seconds, killing your script midway. In AEM 6.5, the browser connection will likely time out, leaving you blind to the script's progress, and tying up a single thread for hours.
Enter the Fast Action Manager (FAM).
FAM is an Adobe API (com.adobe.acs.commons.fam.ActionManager if using ACS Commons, or AEM's internal com.adobe.granite.taskmanagement.FastActionManager) designed to execute bulk tasks across multiple background threads in parallel, without holding open the HTTP request.
To use FAM in the Groovy Console, we leverage the OSGi service registry to grab the ActionManagerFactory. Note: We typically use the ACS AEM Commons FAM as it is more robust and widely adopted for custom tasks.
+-------------------+ +-----------------------+ +-------------------+
| | | | | |
| Groovy Script +------>+ ActionManagerFactory +------>+ Thread Pool |
| (Main Thread) | | (Creates Task) | | (Parallel Exec) |
| | | | | |
+-------------------+ +-----------------------+ +--------+----------+
|
| Work Items
v
+-------------------+
| JCR Nodes |
| (Updated in bulk)|
+-------------------+Here is the pattern for using FAM within a Groovy script:
import com.adobe.acs.commons.fam.ActionManagerFactory
import com.adobe.acs.commons.fam.actions.Actions
// 1. Get the FAM factory from OSGi
def famFactory = getService(ActionManagerFactory.class)
// 2. Create an Action Manager task
def actionManager = famFactory.createTaskManager("Groovy Bulk Update", resourceResolver, 1)
// 3. Define the search query
def query = "SELECT * FROM [cq:Page] WHERE ISDESCENDANTNODE('/content/myproject')"
// 4. Submit the work items to FAM
actionManager.withQueryResults(
query,
"JCR-SQL2",
Actions.retry(5, 500, { resourceResolver, path ->
// THIS CODE RUNS IN PARALLEL BACKGROUND THREADS
def resource = resourceResolver.getResource(path + "/jcr:content")
if (resource != null) {
def modifiableValueMap = resource.adaptTo(ModifiableValueMap.class)
modifiableValueMap.put("migrated", true)
// Note: Actions.retry handles the resourceResolver commit automatically
}
})
)
out.println("FAM Task submitted! Check the FAM dashboard for progress.")By leveraging FAM, your Groovy script finishes in milliseconds, outputting "Task submitted!". The actual heavy lifting happens in the background, fully utilizing AEM's multithreading capabilities and avoiding request timeouts.
The Script Library
Over the years of maintaining enterprise AEM platforms, I have built a vast library of Groovy scripts. Below are 10 highly detailed, ready-to-use scripts that solve complex, real-world problems.
1. Bulk Find and Replace a Property Across 10,000 Pages
The Problem: The marketing team decided to rebrand a specific product name. This name is stored in a page property called productCategory across tens of thousands of product detail pages.
The Solution: Use QueryBuilder to find the pages and batch the JCR saves to prevent memory bloat.
import javax.jcr.Node
def queryMap = [
"type": "cq:PageContent",
"path": "/content/we-retail/us/en/products",
"property": "productCategory",
"property.value": "LegacyBrandName",
"p.limit": "-1"
]
def query = queryBuilder.createQuery(PredicateGroup.create(queryMap), session)
def result = query.getResult()
def hits = result.getHits()
def count = 0
def BATCH_SIZE = 500
def oldName = "LegacyBrandName"
def newName = "NextGenBrand"
out.println("Found ${hits.size()} pages to update.")
hits.each { hit ->
def node = hit.getNode()
if (node.hasProperty("productCategory")) {
node.setProperty("productCategory", newName)
count++
// Save in batches to manage memory
if (count % BATCH_SIZE == 0) {
session.save()
out.println("Processed ${count} nodes...")
}
}
}
// Final save for remaining nodes
session.save()
out.println("Update complete. Total nodes updated: ${count}")2. Find All Pages Missing a Specific Metadata Tag
The Problem: SEO requires that all pages under a specific path have a canonical tag configured. You need a list of all pages that are missing this configuration to hand over to the authoring team. The Solution: Use Groovy to query pages and generate an HTML table in the console output.
def searchPath = "/content/myproject/us/en"
def missingCount = 0
out.println("<h3>Pages missing 'cq:canonicalUrl' property</h3>")
out.println("<table border='1' cellpadding='5'>")
out.println("<tr><th>Page Path</th><th>Title</th><th>Last Modified</th></tr>")
def queryMap = [
"type": "cq:PageContent",
"path": searchPath,
"p.limit": "-1"
]
def query = queryBuilder.createQuery(PredicateGroup.create(queryMap), session)
def hits = query.getResult().getHits()
hits.each { hit ->
def node = hit.getNode()
// Check if property is missing or empty
if (!node.hasProperty("cq:canonicalUrl") || node.getProperty("cq:canonicalUrl").getString().isEmpty()) {
def pagePath = hit.getPath().replace("/jcr:content", "")
def title = node.hasProperty("jcr:title") ? node.getProperty("jcr:title").getString() : "No Title"
def lastMod = node.hasProperty("cq:lastModified") ? node.getProperty("cq:lastModified").getDate().getTime().toString() : "Unknown"
out.println("<tr><td>${pagePath}</td><td>${title}</td><td>${lastMod}</td></tr>")
missingCount++
}
}
out.println("</table>")
out.println("<br><b>Total Missing: ${missingCount}</b>")3. Bulk Replicate/Activate a Specific List of Paths
The Problem: A massive content ingestion just occurred, but the workflow to activate the pages failed. You have a CSV or text list of 5,000 page paths that need to be activated immediately.
The Solution: Inject the Replicator service and iterate over a defined list of paths.
import com.day.cq.replication.Replicator
import com.day.cq.replication.ReplicationActionType
import com.day.cq.replication.ReplicationOptions
def replicator = getService(Replicator.class)
def options = new ReplicationOptions()
// Set to true to avoid synchronous replication hanging the script
options.setSynchronous(false)
// In reality, you might read this from a file, but here we define a list
def pathsToActivate = [
"/content/myproject/us/en/home",
"/content/myproject/us/en/about",
"/content/myproject/us/en/contact"
// ... add thousands more
]
def successCount = 0
def errorCount = 0
pathsToActivate.each { path ->
try {
// Ensure path exists
if (session.nodeExists(path)) {
replicator.replicate(session, ReplicationActionType.ACTIVATE, path, options)
out.println("Activated: ${path}")
successCount++
} else {
out.println("<span style='color:red;'>Path not found: ${path}</span>")
errorCount++
}
} catch (Exception e) {
out.println("<span style='color:red;'>Error activating ${path}: ${e.message}</span>")
errorCount++
}
}
out.println("<b>Finished. Success: ${successCount}, Errors: ${errorCount}</b>")4. Identify and Delete Empty JCR Nodes
The Problem: Bad custom code left thousands of empty nt:unstructured nodes scattered across the DAM structure, degrading query performance.
The Solution: Recursively traverse the JCR, identify nodes with no properties (other than primaryType) and no children, and delete them.
import javax.jcr.Node
import javax.jcr.NodeIterator
def startPath = "/content/dam/myproject"
def deletedCount = 0
def BATCH_SIZE = 500
def deleteEmptyNodes(Node node) {
if (node == null) return
// Traverse children first (bottom-up approach)
NodeIterator children = node.getNodes()
while (children.hasNext()) {
deleteEmptyNodes(children.nextNode())
}
// After children are processed, check if THIS node is now empty
// Only target nt:unstructured to be safe
if (node.getPrimaryNodeType().getName() == "nt:unstructured") {
if (!node.hasNodes()) {
// Check properties. Expecting only jcr:primaryType
def propCount = node.getProperties().getSize()
if (propCount <= 1) {
def path = node.getPath()
node.remove()
deletedCount++
out.println("Deleted empty node: ${path}")
if (deletedCount % BATCH_SIZE == 0) {
session.save()
out.println("--- Batch saved ---")
}
}
}
}
}
def rootNode = session.getNode(startPath)
deleteEmptyNodes(rootNode)
session.save() // final save
out.println("<b>Cleanup complete. Total empty nodes deleted: ${deletedCount}</b>")5. Migrate a Legacy Tag Namespace to a New Namespace
The Problem: The taxonomy team reorganized tags. The namespace legacy-brand: needs to be migrated to global-brand:. You must update the tag definitions AND update every page/asset that uses the old tags.
The Solution: Use the TagManager API for robust tag handling.
import com.day.cq.tagging.TagManager
import com.day.cq.tagging.Tag
def tagManager = resourceResolver.adaptTo(TagManager.class)
def oldNamespaceId = "legacy-brand"
def newNamespaceId = "global-brand"
def oldNamespace = tagManager.resolve(oldNamespaceId + ":")
if (oldNamespace == null) {
out.println("Legacy namespace not found.")
return
}
// 1. Move the tags in the taxonomy tree
// Note: This requires the new namespace to already exist
def newNamespace = tagManager.resolve(newNamespaceId + ":")
if (newNamespace != null) {
oldNamespace.listChildren().each { tag ->
def oldTagId = tag.getTagID()
def newTagId = oldTagId.replace(oldNamespaceId + ":", newNamespaceId + ":")
try {
// Using TagManager to move the tag updates taxonomy and handles some references
// But we often need to manually update content for safety
tagManager.moveTag(tag, newNamespace.getPath() + "/" + tag.getName())
out.println("Moved tag ${oldTagId} to ${newTagId}")
} catch (Exception e) {
out.println("Failed to move ${oldTagId}: ${e.message}")
}
}
}
// 2. Search content for the old tags and replace with new (belt-and-suspenders approach)
def queryMap = [
"path": "/content",
"property": "cq:tags",
"property.value": "${oldNamespaceId}:%",
"property.operation": "like",
"p.limit": "-1"
]
def hits = queryBuilder.createQuery(PredicateGroup.create(queryMap), session).getResult().getHits()
hits.each { hit ->
def node = hit.getNode()
def tagsProp = node.getProperty("cq:tags")
def newTagsList = []
def modified = false
if (tagsProp.isMultiple()) {
tagsProp.getValues().each { val ->
def tagStr = val.getString()
if (tagStr.startsWith(oldNamespaceId + ":")) {
newTagsList.add(tagStr.replace(oldNamespaceId + ":", newNamespaceId + ":"))
modified = true
} else {
newTagsList.add(tagStr)
}
}
if (modified) {
node.setProperty("cq:tags", newTagsList as String[])
}
} else {
def tagStr = tagsProp.getString()
if (tagStr.startsWith(oldNamespaceId + ":")) {
node.setProperty("cq:tags", tagStr.replace(oldNamespaceId + ":", newNamespaceId + ":"))
modified = true
}
}
if (modified) {
out.println("Updated tags on content: ${hit.getPath()}")
}
}
session.save()
out.println("Tag migration complete.")6. Find All Pages Using a Deprecated Component
The Problem: You are preparing for an AEM Cloud Service migration (see my AEM 6.5 to Cloud Service Migration Guide). You need to identify all usages of the deprecated myproject/components/content/heavy-carousel.
The Solution: Query all components matching the sling:resourceType.
def deprecatedResourceType = "myproject/components/content/heavy-carousel"
def searchPath = "/content/myproject"
def queryMap = [
"path": searchPath,
"property": "sling:resourceType",
"property.value": deprecatedResourceType,
"p.limit": "-1"
]
def query = queryBuilder.createQuery(PredicateGroup.create(queryMap), session)
def hits = query.getResult().getHits()
// We want to group by page, as a page might have multiple instances
def pagesUsingComponent = [] as Set
hits.each { hit ->
// Traverse up to find the cq:Page
def currentNode = hit.getNode()
while (currentNode != null && currentNode.getName() != "jcr:root" && currentNode.getPrimaryNodeType().getName() != "cq:Page") {
currentNode = currentNode.getParent()
}
if (currentNode != null && currentNode.getPrimaryNodeType().getName() == "cq:Page") {
pagesUsingComponent.add(currentNode.getPath())
}
}
out.println("<h3>Pages using deprecated component: ${deprecatedResourceType}</h3>")
out.println("Total unique pages: ${pagesUsingComponent.size()}<br><br>")
pagesUsingComponent.each { pagePath ->
out.println("<a href='/editor.html${pagePath}.html' target='_blank'>${pagePath}</a><br>")
}7. Generate a Custom CSV Report of All Users and Groups
The Problem: The compliance team needs an audit of all active AEM users, their email addresses, and the groups they belong to. The Solution: Iterate over the Jackrabbit UserManager and output CSV formatted data that can be copied into Excel.
import org.apache.jackrabbit.api.security.user.UserManager
import org.apache.jackrabbit.api.security.user.Authorizable
import org.apache.jackrabbit.api.security.user.User
import org.apache.jackrabbit.api.security.user.Group
def userManager = resourceResolver.adaptTo(UserManager.class)
// Find all authorizables
def authorizables = userManager.findAuthorizables("jcr:primaryType", "rep:User")
out.println("UserId,FirstName,LastName,Email,IsActive,Groups")
while (authorizables.hasNext()) {
Authorizable auth = authorizables.next()
if (auth instanceof User && !auth.isSystemUser()) {
def userId = auth.getID()
def firstName = auth.hasProperty("profile/givenName") ? auth.getProperty("profile/givenName")[0].getString() : ""
def lastName = auth.hasProperty("profile/familyName") ? auth.getProperty("profile/familyName")[0].getString() : ""
def email = auth.hasProperty("profile/email") ? auth.getProperty("profile/email")[0].getString() : ""
def isActive = !auth.isDisabled()
// Collect groups
def groups = []
def groupIter = auth.declaredMemberOf()
while (groupIter.hasNext()) {
groups.add(groupIter.next().getID())
}
def groupStr = groups.join(" | ")
// Output CSV row
out.println("\"${userId}\",\"${firstName}\",\"${lastName}\",\"${email}\",\"${isActive}\",\"${groupStr}\"")
}
}8. Bulk Asset Metadata Update
The Problem: Thousands of images were uploaded without the required dc:rights metadata, putting the company at legal risk.
The Solution: Query the DAM and update the metadata node. (For more DAM topics, see my AEM Assets Complete Guide).
def queryMap = [
"type": "dam:AssetContent",
"path": "/content/dam/marketing/campaigns/2026",
"p.limit": "-1"
]
def hits = queryBuilder.createQuery(PredicateGroup.create(queryMap), session).getResult().getHits()
def count = 0
def defaultRights = "Copyright 2026 MyCompany All Rights Reserved"
hits.each { hit ->
def assetContentNode = hit.getNode()
if (assetContentNode.hasNode("metadata")) {
def metadataNode = assetContentNode.getNode("metadata")
// Only update if it doesn't already exist
if (!metadataNode.hasProperty("dc:rights")) {
metadataNode.setProperty("dc:rights", defaultRights)
count++
if (count % 200 == 0) {
session.save()
out.println("Updated ${count} assets...")
}
}
}
}
session.save()
out.println("Total assets updated with copyright info: ${count}")9. Clean Up Old Workflow Instances
The Problem: The AEM instance is crashing due to a massive buildup of completed workflow instances under /var/workflow/instances.
The Solution: Use the WorkflowService API to purge workflows older than 30 days. (Review my AEM Workflows Complete Guide for architecture details).
import com.adobe.granite.workflow.WorkflowSession
import com.adobe.granite.workflow.exec.Workflow
def workflowSession = resourceResolver.adaptTo(WorkflowSession.class)
def allWorkflows = workflowSession.getWorkflows(["COMPLETED", "ABORTED"] as String[])
def purgeCount = 0
def thirtyDaysAgo = new Date().time - (30L * 24 * 60 * 60 * 1000)
allWorkflows.each { wf ->
def endTime = wf.getTimeEnded()
if (endTime != null && endTime.getTime() < thirtyDaysAgo) {
// Unfortunately WorkflowSession API doesn't have a direct 'purge' method
// that takes a Workflow object in older APIs, so we delete the JCR node
def instanceId = wf.getId()
if (session.nodeExists(instanceId)) {
session.getNode(instanceId).remove()
purgeCount++
if (purgeCount % 500 == 0) {
session.save()
out.println("Purged ${purgeCount} workflow instances...")
}
}
}
}
session.save()
out.println("Completed. Total old workflows purged: ${purgeCount}")10. Find and Fix Broken Internal Links
The Problem: Authors renamed a root page, breaking hundreds of internal links (href="/content/myproject/old-path.html") embedded in Richtext components.
The Solution: Query all components containing the string, parse the property, and rewrite the link.
def oldPath = "/content/myproject/old-path"
def newPath = "/content/myproject/new-path"
def queryMap = [
"path": "/content/myproject",
"property": "text", // Standard property name for RTE
"property.value": "%${oldPath}%",
"property.operation": "like",
"p.limit": "-1"
]
def hits = queryBuilder.createQuery(PredicateGroup.create(queryMap), session).getResult().getHits()
def count = 0
hits.each { hit ->
def node = hit.getNode()
def textVal = node.getProperty("text").getString()
if (textVal.contains(oldPath)) {
def updatedText = textVal.replaceAll(oldPath, newPath)
node.setProperty("text", updatedText)
count++
out.println("Fixed broken link in component: ${node.getPath()}")
if (count % 100 == 0) session.save()
}
}
session.save()
out.println("Total components repaired: ${count}")AEM Groovy Console Cheat Sheet
Keep these snippets handy for quick reference:
| Task | Code Snippet |
|---|---|
| Get Service | def osgiService = getService(com.example.MyService.class) |
| Adapt Resource | def model = resource.adaptTo(com.example.MyModel.class) |
| Execute Query | def hits = queryBuilder.createQuery(PredicateGroup.create(map), session).getResult().getHits() |
| Save Changes | session.save() (Never forget this!) |
| Get Page | def page = pageManager.getPage("/content/site/en") |
| Get Node | def node = session.getNode("/content/path") |
| Check Property | if(node.hasProperty("myProp")) { ... } |
Best Practices
- Batch Your Saves: If you iterate over 100,000 nodes and call
session.save()once at the end, your AEM instance will throw ajava.lang.OutOfMemoryErrorand crash. Always implement a counter and callsession.save()every 500 or 1,000 nodes. - Use
p.limit = -1Wisely: When using QueryBuilder,-1fetches all results. If you expect a massive dataset, consider pagination or using JCR SQL2 with limits and offsets to process in chunks. - Dry Run Mode: Always build a
dryRun = trueboolean into your scripts. Run the script with it set to true to output what would be changed. Verify the logs, then set it tofalseand run it for real. - Version Control: Do not treat Groovy scripts as disposable. Save your production scripts in a dedicated Git repository (
aem-scripts) so your team can review, reuse, and improve them.
Do's & Don'ts
- DO use the Groovy Console for one-off data migrations, bulk content updates, and ad-hoc reporting.
- DO test your script on a lower environment (Local or Dev) against a similar dataset before running on Staging/Prod.
- DO use the Fast Action Manager (FAM) for any script that takes longer than 30 seconds to execute, especially on AEM as a Cloud Service.
- DON'T use the Groovy Console to build permanent API endpoints or integrations. That is what Sling Servlets and OSGi services are for (see AEM APIs and Integrations Complete Guide).
- DON'T leave the console unsecured. Restrict it strictly to administrators via OSGi configuration.
- DON'T forget to check for
null. Always verify a node or property exists before attempting to operate on it to preventNullPointerExceptionsmidway through a bulk update.
By mastering the AEM Groovy Console, you elevate yourself from an AEM developer to an AEM operator. You stop fearing massive content changes and start managing the repository with absolute control. Keep this script library bookmarked, and you'll be prepared for almost any content emergency that comes your way.
Discussion
Loading discussion…
Try a related tool
Subscribe to the Newsletter
Get the latest articles, tutorials, and tech insights delivered straight to your inbox. No spam, unsubscribe anytime.