{"id":26313378,"url":"https://github.com/wttech/acm","last_synced_at":"2026-03-09T17:29:49.260Z","repository":{"id":282375775,"uuid":"942523848","full_name":"wttech/acm","owner":"wttech","description":"AEM Content Manager (ACM)","archived":false,"fork":false,"pushed_at":"2025-03-14T08:38:34.000Z","size":4990,"stargazers_count":0,"open_issues_count":3,"forks_count":0,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-03-14T09:34:16.713Z","etag":null,"topics":["adobe-experience-manager","aem","aem-tools","aemaacs","cms","content-management-system","groovy","groovy-language","groovy-script","migration"],"latest_commit_sha":null,"homepage":"https://www.vml.com/expertise/enterprise-solutions","language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/wttech.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2025-03-04T08:35:18.000Z","updated_at":"2025-03-14T08:38:37.000Z","dependencies_parsed_at":"2025-03-14T09:44:59.209Z","dependency_job_id":null,"html_url":"https://github.com/wttech/acm","commit_stats":null,"previous_names":["wttech/acm"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wttech%2Facm","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wttech%2Facm/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wttech%2Facm/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wttech%2Facm/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/wttech","download_url":"https://codeload.github.com/wttech/acm/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243719399,"owners_count":20336607,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["adobe-experience-manager","aem","aem-tools","aemaacs","cms","content-management-system","groovy","groovy-language","groovy-script","migration"],"created_at":"2025-03-15T11:15:14.062Z","updated_at":"2026-02-06T11:55:04.932Z","avatar_url":"https://github.com/wttech.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003ca href=\"https://www.vml.com/expertise/enterprise-solutions\" target=\"_blank\"\u003e\n  \u003cpicture\u003e\n    \u003csource srcset=\"docs/vml-logo-white.svg\" media=\"(prefers-color-scheme: dark)\"\u003e\n    \u003cimg src=\"docs/vml-logo-black.svg\" alt=\"VML Logo\" height=\"100\"\u003e\n  \u003c/picture\u003e\n\u003c/a\u003e\n\n[![GitHub tag (latest SemVer)](https://img.shields.io/github/v/tag/wttech/acm)](https://github.com/wttech/acm/releases)\n[![GitHub All Releases](https://img.shields.io/github/downloads/wttech/acm/total)](https://github.com/wttech/acm/releases)\n[![Check](https://github.com/wttech/acm/workflows/Check/badge.svg)](https://github.com/wttech/acm/actions/workflows/check.yml)\n[![Apache License, Version 2.0, January 2004](docs/apache-license-badge.svg)](http://www.apache.org/licenses/)\n\n# AEM Content Manager (ACM)\n\n\u003cp\u003e\n\u003cpicture\u003e\n    \u003csource srcset=\"docs/acm-logo-white.svg\" media=\"(prefers-color-scheme: dark)\"\u003e\n    \u003cimg src=\"docs/acm-logo-black.svg\" alt=\"VML Logo\" height=\"140\"\u003e\n\u003c/picture\u003e\n\u003c/p\u003e\n\n**Manage permissions \u0026 content updates as code.**\n\nACM for Adobe Experience Manager (AEM) streamlines workflows and boosts productivity with an intuitive interface and robust features. It automates bulk content and permission changes, making it ideal for content migration and large-scale permission management. ACM offers an IDE-like experience with code completion, auto-import, and on-the-fly compilation.\n\nIt works seamlessly across AEM on-premise, AMS, and AEMaaCS environments.\n\n## References\n\n* Talk at AdaptTo 2025 Conference - [Can't we just automate this? Permissions \u0026 content updates as-a-code with ACM Tool](https://adapt.to/2025/schedule/cant-we-just-automate-this-permissions-and-content-updates-as-a-code-with-acm-tool) by [Krystian Panek](mailto:krystian.panek@vml.com) \u0026amp; [Tomasz Sobczyk](mailto:tomasz.sobczyk@vml.com)\n\n[![AdaptTo 2025 Video](docs/adaptto-2025-video.jpg)](https://www.youtube.com/watch?v=hGcMWGE7ZiU)\n\n## Screenshots\n\n\u003cimg src=\"docs/screenshot-dashboard.png\" width=\"720\" alt=\"ACM Dashboard\"\u003e\n\n\n## Table of Contents\n\n- [AEM Content Manager (ACM)](#aem-content-manager-acm)\n  - [References](#references)\n  - [Screenshots](#screenshots)\n  - [Table of Contents](#table-of-contents)\n  - [Key Features](#key-features)\n    - [All-in-one Solution](#all-in-one-solution)\n    - [New Approach](#new-approach)\n    - [Content Management](#content-management)\n    - [Permissions Management](#permissions-management)\n    - [Data Imports \\\u0026 Exports](#data-imports--exports)\n  - [Installation](#installation)\n    - [Package Installation](#package-installation)\n    - [Tools Access Configuration](#tools-access-configuration)\n      - [Feature Permissions](#feature-permissions)\n      - [API Permissions](#api-permissions)\n  - [Compatibility](#compatibility)\n  - [Documentation](#documentation)\n    - [Usage](#usage)\n    - [Console](#console)\n    - [Content scripts](#content-scripts)\n      - [Minimal example](#minimal-example)\n      - [Conditions](#conditions)\n      - [Inputs example](#inputs-example)\n      - [Outputs example](#outputs-example)\n      - [Console \\\u0026 logging](#console--logging)\n        - [Simple console output](#simple-console-output)\n        - [Timestamped console output](#timestamped-console-output)\n        - [Logged console output](#logged-console-output)\n      - [ACL example](#acl-example)\n      - [Repo example](#repo-example)\n      - [Abortable example](#abortable-example)\n      - [Script documentation](#script-documentation)\n    - [History](#history)\n    - [Extension scripts](#extension-scripts)\n      - [Example extension script](#example-extension-script)\n    - [Snippets](#snippets)\n      - [Example snippet](#example-snippet)\n    - [Mocks](#mocks)\n    - [Notifications](#notifications)\n  - [Development](#development)\n  - [Releasing](#releasing)\n  - [Authors](#authors)\n  - [Contributing](#contributing)\n  - [License](#license)\n\n## Key Features\n\n### All-in-one Solution\n\nACM is a comprehensive alternative to tools like APM, AECU, AEM Groovy Console, and AC Tool.\nIt leverages the Groovy language, which is familiar to most Java developers, eliminating the need to learn custom YAML syntax or languages/grammars. \nEnjoy a single, painless tool setup in AEM (Adobe Experience Manager) projects with no hooks and POM updates.\n\n### New Approach\n\nExperience a different way of using Groovy scripts. \nACM ensures the instance is healthy before scripts decide when to run: once, periodically, or at an exact date and time. \nExecute scripts in parallel or sequentially, offering unmatched flexibility and control.\n\n### Content Management\n\nEffortlessly migrate pages and components between versions. Ensure content integrity and resolve issues with confidence.\n\n### Permissions Management\n\nApply JCR permissions dynamically. \nManage permissions seamlessly during site creation, blueprinting, and for live copies, language copies, and other AEM-specific replication scenarios.\n\n### Data Imports \u0026 Exports\n\nEffortlessly integrate data from external sources into the JCR repository, enhancing content management capabilities. \nBy simplifying data import implementation, ACM allows developers to focus more on developing better components and presenting data effectively, ensuring a user-friendly experience.\n\n## Installation\n\n### Package Installation\n\nThe ready-to-install AEM packages are available on:\n\n- [GitHub releases](https://github.com/wttech/acm/releases).\n- [Maven Central](https://central.sonatype.com/search?q=dev.vml.es.acm).\n\nThere are two ways to install AEM Content Manager on your AEM (Adobe Experience Manager) instances:\n\n1. **Using the 'all' package:**\n    * Recommended for fresh AEM instances.\n    * This package will also install Groovy Bundles ([groovy](https://mvnrepository.com/artifact/org.apache.groovy/groovy) and [groovy-templates](https://mvnrepository.com/artifact/org.apache.groovy/groovy-templates)).\n2. **Using the 'minimal' package:**\n    * Recommended for AEM instances that already contain some dependencies shared with other tools.\n    * This package does not include Groovy bundles, which can be provided by other tools like [AEM Easy Content Upgrade](https://github.com/valtech/aem-easy-content-upgrade/releases) (AECU) or [AEM Groovy Console](https://github.com/orbinson/aem-groovy-console/releases).\n\nFor AEM On-Premise and AEM Managed Service (AMS) deployments, just install the ACM package using the AEM Package Manager.\n\n:construction: Restart required: Basic ACLs and paths are created via [repo-init](https://sling.apache.org/documentation/bundles/repository-initialization.html) [script](https://github.com/wttech/acm/blob/main/ui.config/src/main/content/jcr_root/apps/acm-config/osgiconfig/config/org.apache.sling.jcr.repoinit.RepositoryInitializer~acmcore.config) after installing the ACM package. A reboot is strongly recommended, as the health checker may block code execution until the restart is complete. :construction:\n\nFor AEMaaCS deployments, embed the ACM package as a part of project-specific 'all' package like other vendor packages in the [AEM Project Archetype](https://github.com/adobe/aem-project-archetype/blob/develop/src/main/archetype/all/pom.xml):\n\nAdjust file 'all/pom.xml':\n\n1. Add dependency to [all](https://central.sonatype.com/artifact/dev.vml.es/acm.all) or [min](https://central.sonatype.com/artifact/dev.vml.es/acm.min) package in the *dependencies* section:\n\n    ```xml\n    \u003cdependency\u003e\n        \u003cgroupId\u003edev.vml.es\u003c/groupId\u003e\n        \u003cartifactId\u003eacm.all\u003c/artifactId\u003e\n        \u003cversion\u003e${acm.version}\u003c/version\u003e\n        \u003ctype\u003ezip\u003c/type\u003e\n    \u003c/dependency\u003e\n    ```\n\n2. Add embedding in *filevault-package-maven-plugin* configuration:\n\n    ```xml\n    \u003cembedded\u003e\n        \u003cgroupId\u003edev.vml.es\u003c/groupId\u003e\n        \u003cartifactId\u003eacm.all\u003c/artifactId\u003e\n        \u003ctype\u003ezip\u003c/type\u003e\n        \u003ctarget\u003e/apps/${appId}-vendor-packages/application/install\u003c/target\u003e\n    \u003c/embedded\u003e\n    ```\n\n    Remember to replace `${acm.version}` and `${appId}` with the actual values.\n\n    Repeat the same for [ui.content.example](https://central.sonatype.com/artifact/dev.vml.es/acm.ui.content.example) package if you want to install demonstrative ACM scripts to get you started quickly.\n\n### Tools Access Configuration\n\nThe default settings are defined in the [repo init OSGi config](https://github.com/wttech/acm/blob/main/ui.config/src/main/content/jcr_root/apps/acm-config/osgiconfig/config/org.apache.sling.jcr.repoinit.RepositoryInitializer~acmcore.config), which effectively restrict access to the tool and script execution to administrators only - a recommended practice for production environments.\n\nIf you require further customization, you can create your own repo init OSGi config to override or extend the default configuration.\n\n#### Feature Permissions\n\nACM supports fine-grained permission control through individual features. This allows you to grant specific capabilities to different user groups without providing full access to ACM tool. For a complete list of available features, see the [ACM features directory](https://github.com/wttech/acm/tree/main/ui.apps/src/main/content/jcr_root/apps/acm/feature).\n\n**Example: Create groups for full and limited access:**\n\n```ini\nservice.ranking=I\"100\"\nscripts=[\"  \n    set ACL for everyone\n        deny jcr:read on /apps/cq/core/content/nav/tools/acm\n        deny jcr:read on /apps/acm\n    end\n\n    create group acm-admins\n    set ACL for acm-admins\n        allow jcr:read on /apps/cq/core/content/nav/tools/acm\n        allow jcr:read on /apps/acm\n    end\n\n    create group acm-script-users\n    set ACL for acm-script-users\n        allow jcr:read on /apps/cq/core/content/nav/tools/acm\n        allow jcr:read on /apps/acm/gui\n        allow jcr:read on /apps/acm/api\n\n        allow jcr:read on /apps/acm/feature/script/list\n        allow jcr:read on /apps/acm/feature/script/view\n        allow jcr:read on /apps/acm/feature/execution/view\n\n        allow jcr:read on /conf/acm/settings/script\n    end\n\"]\n```\n\nLater on when AEM is running, just assign users to the created groups (`acm-admins` or `acm-script-users`) to grant them the corresponding access.\n\n#### API Permissions\n\nAccess to ACM's REST API endpoints is controlled through nodes under `/apps/acm/api`. For a complete list of available endpoints, see the [ACM API directory](https://github.com/wttech/acm/tree/main/ui.apps/src/main/content/jcr_root/apps/acm/api).\n\n**Important:** Code execution requires authorization at three levels: API endpoint, feature, and e.g. script path. Example:\n\n```ini\nset ACL for acm-automation-user\n    allow jcr:read on /apps/acm/api\n    allow jcr:read on /apps/acm/feature\n    allow jcr:read on /conf/acm/settings/script\nend\n```\n\n## Compatibility\n\n| AEM Content Manager | AEM           | Java      | Groovy  |\n| ------------------- | ------------- | --------- | ------- |\n| 1.0.0+              | 6.5.0+, cloud | 8, 11, 21 | 4.0.22+ |\n\nSuch a wide range of compatibility was designed to allow using the tool as a part of the AEM upgrade process, where different AEM and Java versions are involved.\n\nThe tool is compatible with almost all AEM versions, starting from on-premise 6.5.0, including the most recent AEMaaCS (AEM as a Cloud Service) version.\nAlso has been tested across various Java versions, including 8, 11, and the latest 21.\nGroovy version 4.0.22+ is used, which is compatible with all mentioned Java versions.\n\nNote that AEM Content Manager is using Groovy scripts concept. However, it is **not** using [AEM Groovy Console](https://github.com/icfnext/aem-groovy-console). It is done intentionally, because Groovy Console has close dependencies to concrete AEM version.\nAEM Content Manager tool is implemented in a AEM version agnostic way, to make it more universal and more fault-tolerant when AEM version is changing.\n\n## Documentation\n\n### Usage\n\nThe ACM tool helps developers to implement Groovy scripts in AEM projects.\nGroovy code need to at first implemented and tested then persisted in the AEM instance for later deployment.\nTo achieve that, ACM provides a set of features to help you with the development process.\n\n**Groovy code can be run in three ways:**\n\n1. **Ad-hoc using 'Console'**\n   - Code executed in the console is run in the context of the currently logged user to AEM.\n\n2. **Manually executed scripts**\n   - Navigate to the 'Scripts' page and select the 'Manual' tab.\n   - Code executed here also runs in the context of the current user.\n\n3. **Automatically executed scripts**\n   - Navigate to the 'Scripts' page and select the 'Automatic' tab.\n   - Code can be scheduled to run once, periodically, or at an exact date and time. Runs in the context of the system user or impersonated user set in the configuration.\n\n**Rules for executing Groovy code:**\n\n- **Context**: Code can leverage any Java code deployed in the AEM instance as OSGi bundles, including project code.\n- **Health Checks**: ACM performs health checks to ensure the instance is stable before executing scripts. These checks include:\n  - OSGi bundles (with the ability to exclude some to address known issues)\n  - OSGi events occurrence indicating temporal instability\n  - JCR repository paths presence (e.g., `/content/acme`, `/content/dam/acme`)\n\n### Console\n\nThe ACM Console is interactive and offers the following features:\n\n- Execute just-in-time Groovy code.\n- Review the output of the code in real-time.\n- View compilation errors as you type.\n- Access a list of available variables and methods.\n- Utilize code completion assistance for OSGi classes, JCR paths, etc.\n- Quickly insert code templates using snippets.\n\n\u003cimg src=\"docs/screenshot-console-interactive.png\" width=\"720\" alt=\"ACM Console\"\u003e\n\n### Content scripts\n\nContent scripts in ACM are Groovy scripts that can be used to automate various tasks in AEM. \nThese scripts can be placed in specific locations within the AEM repository to control their execution behavior.\n\n- `/conf/acm/settings/script/automatic/{project}`: Automatically executed scripts on instance boot (usually run once after deployment) or on scheduled intervals defined by `scheduleRun()` method. Specific conditions could be narrowed by `canRun()` method.\n- `/conf/acm/settings/script/manual/{project}`: Manually executed scripts (usually with inputs), run under specific circumstances by platform administrators.\n\n#### Minimal example\n\nBelow is a minimal example of a Groovy script that prints \"Hello World!\" to the console.\n\n```groovy\nboolean canRun() {\n    return conditions.always()\n}\n\nvoid doRun() {\n    println \"Hello World!\"\n}\n```\n\nThe `canRun()` method is used to determine if the script should be executed.\nThe `doRun()` method contains the actual code to be executed.\n\nNotice that the script on their own decide when to run without a need to specify any additional metadata. In that way the-sky-is-the-limit. You can run the script once, periodically, or at an exact date and time.\nThere are many built-in, ready-to-use conditions available in the `conditions` [service](https://github.com/wttech/acm/blob/main/core/src/main/java/dev/vml/es/acm/core/code/Conditions.java).\n\n#### Conditions\n\nConditions determine when automatic scripts should execute. The `conditions` [service](https://github.com/wttech/acm/blob/main/core/src/main/java/dev/vml/es/acm/core/code/Conditions.java) provides many useful methods:\n\n- `conditions.always()` - Always execute on every trigger. Most commonly used in console and manual scripts where execution is triggered directly by users.\n- `conditions.never()` - Never execute. Useful for temporarily disabling scripts.\n- `conditions.changed()` - Execute when script content changed or when instance changed after a failure. Automatically retries failed executions after deployments, making it more suitable for production scenarios than `once()`.\n- `conditions.contentChanged()` - Execute when script content changed or when never executed before. Does not consider instance state changes.\n- `conditions.instanceChanged()` - Execute when instance state changed (OSGi bundle checksums changed or ACM bundle restarted). Useful for detecting deployments or restarts.\n- `conditions.retryIfInstanceChanged()` - Execute when instance state changed and previous execution failed. Combines instance change detection with failure retry logic.\n- `conditions.once()` - Execute only once, when never executed before. Does not automatically retry after failures. Works well for initialization scripts that should not be repeated.\n- `conditions.notSucceeded()` - Execute if previous execution wasn't successful. Retries execution until it succeeds, ignoring script content and instance state changes.\n- `conditions.isInstanceAuthor()` / `conditions.isInstancePublish()` - Execute only on specific instance types (author or publish).\n- `conditions.isInstanceRunMode(\"dev\")` - Execute only when instance has specific run mode.\n- `conditions.isInstanceOnPrem()` - Execute only on on-premise AEM instances.\n- `conditions.isInstanceCloud()` - Execute only on cloud-based AEM instances (AEMaaCS).\n- `conditions.isInstanceCloudSdk()` - Execute only on AEM Cloud SDK (local development environment).\n- `conditions.isInstanceCloudContainer()` - Execute only on AEM Cloud Service containers (non-SDK cloud instances).\n\n**Example usage:**\n\n```groovy\nboolean canRun() {\n    return conditions.once()\n}\n\nvoid doRun() {\n    out.info \"Removing deprecated properties from pages...\"\n    repo.get(\"/content/acme\").query(\"n.[sling:resourceType=acme/component/page]\").each { page -\u003e\n        page.removeProperty(\"deprecatedProperty\")\n    }\n    out.success \"Removed deprecated properties successfully.\"\n}\n```\n\nFor the complete list of available conditions and their behavior, see the [Conditions.java source code](https://github.com/wttech/acm/blob/main/core/src/main/java/dev/vml/es/acm/core/code/Conditions.java).\n\n#### Inputs example\n\nScripts could accept inputs, which are passed to the script when it is executed.\n\n```groovy\nvoid describeRun() {\n    inputs.string(\"name\") { value = \"John\" }\n    inputs.string(\"surname\") { value = \"Doe\" }\n}\n\nboolean canRun() {\n    return conditions.always()\n}\n\nvoid doRun() {\n    println \"Hello ${inputs.value('name')} ${inputs.value('surname')}!\"\n}\n```\n\nThe `describeRun()` method is used to define the inputs that can be passed to the script.\nThe `inputs` service is used to define the inputs that can be passed to the script.\nWhen the script is executed, the inputs are passed to the `doRun()` method.\n\nThere are many built-in input types to use handling different types of data like string, boolean, number, date, file, etc. Just check `inputs` [service](https://github.com/wttech/acm/blob/main/core/src/main/java/dev/vml/es/acm/core/code/Inputs.java) for more details.\n\n\u003cimg src=\"docs/screenshot-content-script-inputs.png\" width=\"720\" alt=\"ACM Content Script Inputs\"\u003e\n\nBe inspired by reviewing examples like [page thumbnail script](https://github.com/wttech/acm/blob/main/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-202_page-thumbnail.groovy) which allows user to upload a thumbnail image and set it as a page thumbnail with only a few clicks and a few lines of code.\n\n#### Outputs example\n\nScripts can generate output files that can be downloaded after execution.\n\nThe following example of the content script demonstrates how to generate a CSV report as an output file using the `outputs` [service](https://github.com/wttech/acm/blob/main/core/src/main/java/dev/vml/es/acm/core/code/Outputs.java).\n\nThere is no limitation on the number of output files that can be generated by a script. Each output file can have its own label, description, and download name. All outputs are persisted in the history, allowing you to review and download them later.\n\n```groovy\nboolean canRun() {\n    return conditions.always()\n}\n\nvoid doRun() { \n    log.info \"Users report generation started\"\n\n    def report = outputs.file(\"report\") {\n        label = \"Report\"\n        description = \"Users report generated as CSV file\"\n        downloadName = \"report.csv\"\n    }\n\n    def users = [\n        [name: \"John\", surname: \"Doe\", birth: \"1991\"],\n        [name: \"Jane\", surname: \"Doe\", birth: \"1995\"],\n        [name: \"Jack\", surname: \"Smith\", birth: \"1988\"]\n    ] \n    for (def user : users) {\n        report.out.println(\"${user.name},${user.surname},${user.birth}\")\n    }\n\n    log.info \"Users report generation ended successfully\"\n}\n```\n\n\u003cimg src=\"docs/screenshot-content-script-outputs.png\" width=\"720\" alt=\"ACM Content Script Outputs\"\u003e\n\nThere is also available text output type, which allows you to generate markdown or any text with syntax highlighting (JSON, XML, YAML, etc.). This may be useful to allow users to quickly open links to e.g. just created pages, copy-paste generated configuration and use it somewhere else.\n\n```groovy\noutputs.text(\"summary\") { \n    label = \"Summary\"\n    value = \"\"\"\n    - Total Users: ${totalUsers}\n    - Active Users: ${activeUsers}\n    - Inactive Users: ${inactiveUsers}\n    - Generated At: ${new Date()}\n    \"\"\".stripIndent().trim()\n}\n\noutputs.text(\"configJson\") { \n    label = \"Configuration\"\n    value = '''\n    {\n        \"setting1\": true,\n        \"setting2\": \"value\",\n        \"setting3\": 10\n    }\n    '''.stripIndent().trim()\n    language = \"json\"\n}\n```\n\n#### Console \u0026 logging\n\nScripts provide three different ways to write messages to the console and logs, each serving different purposes:\n\n##### Simple console output\n\nUse `println` or `printf` for simple console output without timestamps or log levels. This is useful for quick debugging or generating simple text output.\n\n```groovy\nvoid doRun() {\n    println \"Simple message without timestamp\"\n    printf \"Formatted: %s = %d\\n\", \"count\", 42\n}\n```\n\n##### Timestamped console output\n\nUse `out.error()`, `out.warn()`, `out.success()`, `out.info()`, `out.debug()` to write messages to the console with timestamps and log levels. These messages appear only in the execution console and are not persisted to AEM logs.\n\n```groovy\nvoid doRun() {\n    out.error \"Failed to process resource: ${resource.path}\"\n    out.warn \"Resource ${resource.path} is missing required property\"\n    out.success \"Resource ${resource.path} processed successfully\"\n    out.info \"Processing started\"\n}\n```\n\n##### Logged console output\n\nUse `log.error()`, `log.warn()`, `log.info()`, `log.debug()`, `log.trace()` to write messages both to the console and to AEM logs (e.g., error.log). This is recommended for production scripts where you need persistent log records.\n\n```groovy\nvoid doRun() {\n    log.info \"Doing regular stuff\"\n    \n    try {\n        // ... risky logic\n        log.info \"Doing risky stuff ended\"\n    } catch (Exception e) {\n        log.error \"Doing risky stuff failed: ${e.message}\", e\n    }\n}\n```\n\n**Best practices:**\n\n- Use `println` / `printf` for quick debugging or simple text generation\n- Use `out.*` for console-only feedback during script execution (progress indicators, status updates)\n- Use `log.*` for important events that should be persisted in AEM logs (errors, warnings, critical operations)\n\n#### ACL example\n\nThe following example of the automatic script demonstrates how to create a user and a group, assign permissions, and add members to the group using the [ACL service](https://github.com/wttech/acm/blob/main/core/src/main/java/dev/vml/es/acm/core/acl/Acl.java) (`acl`).\n\n```groovy\ndef scheduleRun() {\n    return schedules.cron(\"0 10 * ? * * *\") // every hour at minute 10\n}\n\nboolean canRun() {\n    return conditions.always()\n}\n\nvoid doRun() {\n    log.info \"ACL setup started\"\n\n    def acmeService = acl.createUser { id = \"acme.service\"; systemUser(); skipIfExists() }.tap {\n        // purge()\n        allow { path = \"/content\"; permissions = [\"jcr:read\", \"jcr:write\"] }\n    }\n    def johnDoe = acl.createUser { id = \"john.doe\"; fullName = \"John Doe\"; password = \"ilovekittens\"; skipIfExists() }.tap {\n        // purge()\n        allow(\"/content\", [\"jcr:read\"])\n    }\n    acl.createGroup { id = \"test.group\" }.tap {\n        // removeAllMembers()\n        addMember(acmeService)\n        addMember(johnDoe)\n    }\n\n    log.info \"ACL setup done\"\n}\n```\n\nOperations done by `acl` service are idempotent, so you can run the script multiple times without worrying about duplicates, failures, or other issues.\nLogging is very descriptive, allowing you to see what was done and what was skipped.\n\nACL scripts can be scheduled to run at regular intervals, automatically adapting permissions to the evolving, project-specific structure of your AEM content.\n\n\u003cimg src=\"docs/screenshot-content-script-acl-output.png\" width=\"720\" alt=\"ACM ACL Script Output\"\u003e\n\n#### Repo example\n\nYou can leverage the [repository service](https://github.com/wttech/acm/blob/main/core/src/main/java/dev/vml/es/acm/core/repo/Repo.java) (`repo`) to efficiently perform JCR operations such as reading, writing, and deleting nodes with concise, expressive code.\nThe out-of-the-box AEM API often requires extensive boilerplate code and can behave unpredictably in certain scenarios. The [RepoResource](https://github.com/wttech/acm/blob/main/core/src/main/java/dev/vml/es/acm/core/repo/RepoResource.java) API streamlines these operations, making repository programming more enjoyable, concise, and reliable.\n\nThe repo service abstracts away the complexity of managing dry-run and auto-commit behaviors—features that are often error-prone and cumbersome to implement manually — ensuring safe, predictable, and streamlined repository operations.\n\n```groovy\nvoid describeRun() {\n    inputs.bool(\"dryRun\") { value = true; switcher(); description = \"Do not commit changes to the repository\" }\n    inputs.bool(\"clean\") { value = true; switcher(); description = \"Finally delete all created resources\" }\n}\n\nvoid doRun() {\n    repo.dryRun(inputs.value(\"dryRun\")) {\n        log.info \"Creating a folder structure in the temporary directory of the repository.\"\n        def dataFolder = repo.get(\"/tmp/acm/demo/data\").ensureFolder()\n        for (int i = 0; i \u003c 5; i++) {\n            def child = dataFolder.child(\"child-${i+1}\").save([\"foo\": \"bar\"])\n            child.updateProperty(\"foo\") { v -\u003e v.toUpperCase() }\n        }\n        log.info \"Folder '${dataFolder.path}' has now ${dataFolder.descendants().count()} descendant(s).\"\n\n        log.info \"Creating a post in the temporary directory of the repository.\"\n        def postFolder = repo.get(\"/tmp/acm/demo/posts\").ensureFolder()\n        def post = postFolder.child(\"hello-world.yml\").saveFile(\"application/x-yaml\") { output -\u003e\n            formatter.yaml.write(output, [\n                    title: \"Hello World\",\n                    description: \"This is a sample post.\",\n                    tags: [\"sample\", \"post\"]\n            ])\n        }\n        log.info \"Post '${post.path}' has been created at ${post.property(\"jcr:created\", java.time.LocalDateTime)}\"\n\n        if (inputs.value(\"clean\")) {\n            dataFolder.delete()\n            postFolder.delete()\n        }\n    }\n}\n```\n\n\u003cimg src=\"docs/screenshot-content-script-repo-output.png\" width=\"720\" alt=\"ACM ACL Repo Output\"\u003e\n\n#### Abortable example\n\nFor long-running scripts that process many nodes, it's important to support graceful abortion. This allows users to stop the script execution without leaving the repository in an inconsistent state.\n\n```groovy\nvoid doRun() {\n    repo.queryRaw(\"SELECT * FROM [nt:base] WHERE ISDESCENDANTNODE('/content/acme/us/en')\").forEach { resource -\u003e\n        // Safe point\n        context.checkAborted()\n        \n        // Process resource\n        // TODO resource.save() etc.\n    }\n}\n```\n\nAlternatively, you can use `context.isAborted()` for manual control:\n\n```groovy\nvoid doRun() {\n    def assets = repo.queryRaw(\"SELECT * FROM [dam:Asset] WHERE ISDESCENDANTNODE('/content/dam')\").iterator()\n    for (asset in assets) {\n        if (context.isAborted()) {\n            // Do clean when aborted\n            break\n        }\n    }\n    // Still remember to propagate abort status\n    context.checkAborted()\n}\n```\n\n#### Script documentation\n\nScripts can include metadata and documentation using block comments with YAML frontmatter. This metadata is displayed in the ACM UI to help users understand the script's purpose and configuration.\n\n**Prerequisites:**\n- Use regular block comments `/* */` (not JavaDoc `/** */`)\n- Place the comment at the top of the file OR after import/package statements\n- Must be followed by a blank line to separate from code\n\n**Example with metadata:**\n\n```groovy\n/*\n---\nversion: '1.0'\nauthor: john.doe@acme.com\nschedule: Every hour at 10 minutes past the hour\ncategory: security\ntags: ['content', 'migration', 'security']\n---\nCreates content author groups for each tenant-country-language combination.\n\nThe groups are named in the format: `{tenant}-{country}-{language}-content-authors`.\nEach group is granted read, write, and replicate permissions on the corresponding content and DAM paths.\n\n\\```mermaid\n---\nconfig:\n  look: handDrawn\n  theme: base\n---\ngraph LR\n    A[Scan Tenants] --\u003e B[Find Countries]\n    B --\u003e C[Find Languages]\n    C --\u003e D[Create Author Groups]\n    D --\u003e E[Grant Permissions]\n\\```\n*/\n\nboolean canRun() {\n    return conditions.changed()\n}\n\nvoid doRun() {\n    // implementation\n}\n```\n\n\u003e **Note:** In actual Groovy scripts, use regular triple backticks for Mermaid blocks without escaping. The backslashes above are only needed for README rendering.\n\n**Metadata and visual features:**\n\nAny custom fields can be defined in the YAML frontmatter. Common examples include:\n- `version` - Script version number\n- `author` - Script author email  \n- `schedule` - Human-readable schedule description\n- `category` - Script category for organization\n- `tags` - Array of tags, rendered as neutral badges in the UI for easy categorization\n\nAdditional visual features:\n- **Markdown formatting** ([GitHub Flavored Markdown](https://github.github.com/gfm/)) is supported throughout all the metadata custom fields and description.\n- **Mermaid diagrams** can be embedded to visualize script logic (supports [Mermaid syntax](https://mermaid.js.org/intro/syntax-reference.html))\n\nFor complete examples, see the [example scripts directory](ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example).\n\n\u003cimg src=\"docs/screenshot-metadata.png\" width=\"720\" alt=\"ACM Script Metadata\"\u003e\n\n\n### History\n\nAll code executions are logged in the history. You can see the status of each execution, including whether it was successful or failed. The history also provides detailed logs for each execution, including any errors that occurred.\nOriginal code is stored in the history, so you can always refer back to it if needed.\nComplete output as well as input values are also included to achieve full traceability.\n\n\u003cimg src=\"docs/screenshot-history.png\" width=\"720\" alt=\"ACM History - Executions\"\u003e\n\u003cimg src=\"docs/screenshot-history-execution-code.png\" width=\"720\" alt=\"ACM History - Execution Code\"\u003e\n\u003cimg src=\"docs/screenshot-history-execution-output.png\" width=\"720\" alt=\"ACM History - Execution Output\"\u003e\n\n### Extension scripts\n\nTo add own code binding or hook into execution process, you can create your own extension Groovy scripts and place them at path like `/conf/acm/settings/script/extension/acme/main.groovy`.\n\n#### Example extension script\n\n```groovy\nimport dev.vml.es.acm.core.code.ExecutionContext\nimport dev.vml.es.acm.core.code.Execution\n\nvoid prepareRun(ExecutionContext executionContext) {\n    executionContext.variable(\"acme\", new AcmeFacade())\n}\n\nvoid completeRun(Execution execution) {\n    if (execution.status.name() == 'FAILED') {\n        log.error \"Something nasty happened with '${execution.executable.id}'!\"\n        // TODO send notification on Slack, MS Teams, etc using HTTP client / WebAPI\n    }\n}\n\nclass AcmeFacade {\n    def now() {\n        return new Date()\n    }\n}\n```\n\n### Snippets\n\nACM provides a set of snippets that can be used to quickly insert code templates into your scripts. \nDespite out-of-the-box snippets, you can create your own snippets and share them with your team.\n\nJust create a YAML files in the `/conf/acm/settings/snippet/available/{project}` folder and add your code there.\n\n#### Example snippet\n\nNote that snippets could contain placeholders, which are replaced with actual values when the snippet is inserted into the script.\nAlso, snippet documentation could use [GitHub Flavored Markdown](https://github.github.com/gfm/) syntax, which is later rendered in the UI. As a consequence, you can use HTML tags as well, provide links, images, etc.\n\nLet's assume a snippet located at path `/conf/acm/settings/snippet/available/acme/hello.yml` with the following content:\n\n```yaml\ngroup: Acme\nname: acme_hello\ncontent: |\n  println \"Hello ${1:message} in ACME project!\" }\ndocumentation: |\n  Prints a greeting message in the ACME project.\n```\n\n\u003cimg src=\"docs/screenshot-snippets.png\" width=\"720\" alt=\"ACM Snippets\"\u003e\n\n### Mocks\n\nACM has incorporated the [AEM Stubs tool](https://github.com/wttech/aem-stubs), which allows you to create mock HTTP responses for testing purposes also via Groovy Scripts.\nThis feature is disabled by default, but you can enable it in the [OSGi configuration](https://github.com/wttech/acm/blob/main/core/src/main/java/dev/vml/es/acm/core/mock/MockHttpFilter.java).\n\n\u003cimg src=\"docs/screenshot-scripts-mock-tab.png\" width=\"720\" alt=\"ACM Mocks - List\"\u003e\n\u003cimg src=\"docs/screenshot-scripts-mock-code.png\" width=\"720\" alt=\"ACM Mocks - Code\"\u003e\n\n### Notifications\n\nACM offers a flexible notification service supporting multiple channels, including Slack and Microsoft Teams, with no additional coding required.\n\nTo receive notifications about automatic code executions, simply configure a notifier with a unique ID (`acm`) in the OSGi configuration.\n\nFor Slack integration, create a file at *ui.config/src/main/content/jcr_root/apps/{project}/osgiconfig/config/dev.vml.es.acm.core.notification.slack.SlackFactory.config* with the following content:\n\n```ini\nenabled=B\"true\"\nid=\"acm\"\nwebhookUrl=\"https://hooks.slack.com/services/XXXXXXXXX/YYYYYYYYYYY/ZZZZZZZZZZZZZZZZZZZZZZZZ\"\ntimeoutMillis=I\"5000\"\n```\nTo customize notifications triggered by the [executor service](https://github.com/wttech/acm/blob/main/core/src/main/java/dev/vml/es/acm/core/code/Executor.java#L32), use its OSGi configuration. This allows you to control which script executions should trigger notifications and which should be excluded, providing fine-grained management over notification behavior.\n\nThe notification service is a general-purpose feature that can be used for any kind of messaging, not just notifications related to ACM code execution. You can also define multiple notifiers with different IDs to target various channels or teams. In your Groovy scripts or project-specific OSGi bundles, use the `notifier` [service](https://github.com/wttech/acm/blob/main/core/src/main/java/dev/vml/es/acm/core/notification/NotificationManager.java) to send messages to a specific notifier or the default one:\n\n```groovy\nnotifier.sendMessageTo(\"acme\", \"ACME Project Notifications\", \"An important event occurred.\")\nnotifier.sendMessage(\"ACME Project Notifications\", \"Let's start the day with a coffee!\") // uses the 'default' notifier\n```\n\n## Development\n\n1. All-in-one command (incremental building and deployment of 'all' distribution, both backend \u0026 frontend)\n\n    ```shell\n    sh taskw develop:all\n    ```\n\n2. Example contents\n\n    ```shell\n    sh taskw develop:content:example\n    ```\n\n3. Backend only\n\n    ```shell\n    sh taskw develop:core\n    ```\n\n4. Frontend only with production build mode\n\n    ```shell\n    sh taskw develop:frontend\n    ```\n\n5. Frontend only with dev build mode (live reloading)\n\n    ```shell\n    sh taskw develop:frontend:dev\n    ```\n\n## Releasing\n\n1. To check the last release version, run:\n\n    ```shell\n    sh taskw release\n    ```\n \n2. To release a new version, run:\n\n    ```shell\n    sh taskw release -- \u003cnew-version\u003e\n    ```\n\n## Authors\n\n- Founder, owner, and maintainer: [Krystian Panek](mailto:krystian.panek@vml.com)\n- Consultancy, tests: [Tomasz Sobczyk](mailto:tomasz.sobczyk@vml.com), [Jakub Przybytek](mailto:jakub.przybytek@vml.com)\n- Developers: [Mariusz Pacyga](mailto:mariusz.pacyga@vml.com), [Dominik Przybył](mailto:dominik.przybyl@vml.com), [Kamil Orwat](mailto:kamil.orwat@vml.com)\n- Contributors: [\u0026lt;see all\u0026gt;](https://github.com/wttech/aemc/graphs/contributors)\n\n## Contributing\n\nIssues reported or pull requests created will be very appreciated.\n\n1. Fork plugin source code using a dedicated GitHub button.\n2. Do code changes on a feature branch created from *main* branch.\n3. Create a pull request with a base of *main* branch.\n\n## License\n\n**AEM Content Manager** is licensed under the [Apache License, Version 2.0 (the \"License\")](https://www.apache.org/licenses/LICENSE-2.0.txt)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwttech%2Facm","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fwttech%2Facm","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwttech%2Facm/lists"}