Adobe AEM

AEM Groovy Console: The Complete Guide & Script Library

19 min read

Master the AEM Groovy Console with this comprehensive guide covering installation, security, implicit variables, Fast Action Manager (FAM), and a massive library of ready-to-use production scripts.

AEMGroovyScriptsBackendReference
AEM Groovy Console: The Complete Guide & Script Library

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:

  1. Manual Update: Impossible at scale.
  2. JCR SQL2 Query: Great for finding nodes, but cannot update them.
  3. 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.
  4. 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:

  1. Restrict Access: It must be locked down to the administrators group.
  2. 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).
  3. 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:

  1. Never allow authors: Do not grant access to standard content authors or even power authors.
  2. Enable Auditing: Ensure audit.disabled is false. The console logs every script execution to /var/audit/groovyconsole, so you have a trail of who ran what, and when.
  3. Disable in Publish: The Groovy Console should absolutely never be deployed to the Publish tier. Ensure the dependency is only included in your author runmode 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:

VariableTypeDescription
sessionjavax.jcr.SessionThe JCR Session of the user executing the script. Use for low-level JCR operations.
resourceResolverorg.apache.sling.api.resource.ResourceResolverThe Sling Resource Resolver. Use for adapting to models or standard resource manipulation.
pageManagercom.day.cq.wcm.api.PageManagerThe AEM Page Manager for creating, moving, and activating pages.
queryBuildercom.day.cq.search.QueryBuilderThe AEM Query Builder service for executing JCR searches.
nodeBuildercom.day.cq.search.QueryBuilderGroovy's NodeBuilder for rapidly creating JCR node structures natively.
logorg.slf4j.LoggerSLF4J logger instance for writing to error.log.
outjava.io.PrintWriterThe 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}")

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:

TaskCode Snippet
Get Servicedef osgiService = getService(com.example.MyService.class)
Adapt Resourcedef model = resource.adaptTo(com.example.MyModel.class)
Execute Querydef hits = queryBuilder.createQuery(PredicateGroup.create(map), session).getResult().getHits()
Save Changessession.save() (Never forget this!)
Get Pagedef page = pageManager.getPage("/content/site/en")
Get Nodedef node = session.getNode("/content/path")
Check Propertyif(node.hasProperty("myProp")) { ... }

Best Practices

  1. Batch Your Saves: If you iterate over 100,000 nodes and call session.save() once at the end, your AEM instance will throw a java.lang.OutOfMemoryError and crash. Always implement a counter and call session.save() every 500 or 1,000 nodes.
  2. Use p.limit = -1 Wisely: When using QueryBuilder, -1 fetches all results. If you expect a massive dataset, consider pagination or using JCR SQL2 with limits and offsets to process in chunks.
  3. Dry Run Mode: Always build a dryRun = true boolean into your scripts. Run the script with it set to true to output what would be changed. Verify the logs, then set it to false and run it for real.
  4. 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 prevent NullPointerExceptions midway 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.

Share this article

Discussion

By commenting you agree to the Privacy Policy. Guest comments are reviewed before they appear.

Loading discussion…

Subscribe to the Newsletter

Get the latest articles, tutorials, and tech insights delivered straight to your inbox. No spam, unsubscribe anytime.

Back to Blog