Asset Workflow customisation in AEM as a Cloud Service
Jorge Chércoles, July 2, 2024
The functionality of Assets workflows is expansive from the simple approval workflow to the complex one, encapsulating multiple data points and destinations. When it comes to using Adobe Experience Manager (AEM) workflows, developers and authors will find a lot of what they need already built in but for some use cases, like the one outlined below, customization is necessary.
Out of the Box vs Custom Workflow Solutions
AEM provides two out-of-the-box ways of adding extra information in a workflow process:
- Dialog Participant Step: Requires the interaction of the user from the inbox and prevents the workflow from continuing until the user approves/rejects the task.
- Workflow process arguments: Requires a custom parser (e.g: https://joao.ws/parsing-aem-workflow-arguments/) to extract the information and needs to be hardcoded in the workflow. If this information needs to vary between executions, it has to be modified from the “Workflow admin console”.
Both have their use cases, and can be used when needed. However, there is another use case that is not covered by AEM: Dynamic configuration per workflow execution directly before launching.
In other words, letting the author (or executor) of the workflow make a decision at the time that they are launching the workflow. Solving for this use case required integrating custom forms within the launch step when executing workflows for one or more assets. How we did this is outlined below.
Implementation
Step 1 - Custom Workflow Process
Like any other custom workflow process implementation, we need to create a Java class that implements the interface com.adobe.granite.workflow.exec.WorkflowProcess.
We will then access the workflow metadata using the WorkItem.getWorkflow().getMetadataMap() method, where the key is the input name of the dialog field.
@Component(service = WorkflowProcess.class, property = {"process.label=Dummy Workflow Process"})
public class DummyWorkflow implements WorkflowProcess {
private static final Logger LOGGER = LoggerFactory.getLogger(DummyWorkflow.class);
public void execute(WorkItem workItem, WorkflowSession wfSession, MetaDataMap metaDataMap) {
MetaDataMap map = workItem.getWorkflow().getMetaDataMap();
LOGGER.info("DummyWorkflow - init");
map.forEach((key, value) -> LOGGER.info("DummyWorkflow - MetaDataMap: {} -> {}", key, value));
}
}
This will allow us to select our custom Workflow Process inside a "Process Step" in our workflow model.
For the workflow model example refer to https://bitbucket.org/3SHARE/aem-workflow-customisation/src/main/ui.content/src/main/content/jcr_root/var/workflow/models/dummy-workflow-model.xml.
Step 2 - Custom dialog | Workflow admin console
When running a workflow from the “Workflow admin console”, the resource that handles the rendering can be found under /libs/cq/workflow/admin/console/content/models/runmodeldialog.
If we inspect the node in the JCR repository, we can find a descendant node items/form/items/extraItems/items/include with a reference to a resource under cq/workflow/extensions/models/runmodeldialog.
Find below an example of an extension with a custom form:
<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0"
xmlns:granite="http://www.adobe.com/jcr/granite/1.0"
xmlns:jcr="http://www.jcp.org/jcr/1.0"
jcr:primaryType="nt:unstructured"
jcr:title="Dummy Workflow"
sling:resourceType="granite/ui/components/coral/foundation/form/container">
<items jcr:primaryType="nt:unstructured">
<dummy-container
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/container">
<granite:data
jcr:primaryType="nt:unstructured"
workflow-node="${requestPathInfo.resourcePath}"
workflow="/var/workflow/models/dummy-workflow-model"/>
<granite:rendercondition
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/renderconditions/or">
<workflow-models
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/renderconditions/simple"
expression="${requestPathInfo.suffix == '/var/workflow/models/dummy-workflow-model'}"/>
<assets
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/renderconditions/simple"
expression="${requestPathInfo.suffix == null}"/>
</granite:rendercondition>
<items jcr:primaryType="nt:unstructured">
<dummy-select
jcr:primaryType="nt:unstructured"
granite:rel="cq-common-admin-timeline-toolbar-actions-workflow-select"
sling:resourceType="granite/ui/components/coral/foundation/form/select"
required="{Boolean}true"
emptyText="Dummy Select"
fieldLabel="Dummy Select"
name="dummy-select">
<items jcr:primaryType="nt:unstructured">
<item1 jcr:primaryType="nt:unstructured" text="Item 1" value="item-1"/>
<item2 jcr:primaryType="nt:unstructured" text="Item 2" value="item-2"/>
<item3 jcr:primaryType="nt:unstructured" text="Item 3" value="item-3"/>
<item4 jcr:primaryType="nt:unstructured" text="Item 4" value="item-4"/>
</items>
</dummy-select>
<dummy-text
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
fieldLabel="Dummy Text"
name="dummy-text"/>
</items>
</dummy-container>
</items>
</jcr:root>
For a complete example, refer to: https://bitbucket.org/3SHARE/aem-workflow-customisation/src/main/ui.apps/src/main/content/jcr_root/apps/cq/workflow/extensions/models/runmodeldialog/
If we want more fields, we can add them here, following the same structure as the <dummy-container/>
. The most important nodes are the granite:data
and granite:rendercondition
.
granite:data
exposes two attributes. These two attributes will be used later on when we create the clientlib.
data-workflow-node
exposes the node path. It will be used to request the form directly via HTTP GET.data-workflow
exposes the workflow path associated to this form.
granite:rendercondition
defines when this form will be rendered. These two render conditions are required to make it work. In case you need custom render conditions, refer to: https://joao.ws/aem-granite-render-conditions/
workflow-models
is true when the suffix of the current request is equal to our workflow. When the dialog is requested from the “Workflow admin console”, the request suffix stores the requested workflow model.assets
is true when there is no suffix in the request. When the dialog is requested from the “Timeline side panel”, we need to render the form. Visibility will be handled in the clientlib.
Once the node is deployed, the form should start appearing in the “Workflow admin console”, as shown in the image at the beginning of this section.
ℹ️ Custom forms can be built the same way we do with the component dialogs, however the show/hide feature would not work. Although, If that feature is needed you can check out our previous post about it: https://blog.3sharecorp.com/show-and-hide-dialog-fields-based-on-any-field-type 😉
Step 3 - Custom dialog | Timeline side panel
ℹ️ Note: Bulk action can also be done when selecting more than one asset in the assets page.
Unlike with the “Workflow admin console”, the “Asset timeline side panel” does not support the include of custom forms in the dialog. Therefore, we have to overlay the dialog under /libs/dam/gui/coral/content/commons/sidepanels/timeline/items/toolbar/items/workflows/items/form
For a code example, refere to https://bitbucket.org/3SHARE/aem-workflow-customisation/src/main/ui.apps/src/main/content/jcr_root/apps/dam/gui/coral/content/commons/sidepanels
Step 4 - Custom dialog | Create workflow button
As with the “Asset timeline side panel,” we have to enable the inclusion of custom forms in the dialog. For this, we have to overlay the dialog under /libs/dam/gui/content/commons/createworkflowdialog.
For a code example, refer to https://bitbucket.org/3SHARE/aem-workflow-customisation/src/main/ui.apps/src/main/content/jcr_root/apps/dam/gui/content/commons/createworkflowdialog/
Step 5 - Custom clientlib
The custom form in the “Assets timeline side panel” and the “Create workflow dialog” need to appear only for our workflow. In order to do this we need to create a custom clientlib with the same category and dependencies as /libs/dam/gui/coral/components/admin/timeline/events/workflow/clientlibs/workflow
<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0"
jcr:primaryType="cq:ClientLibraryFolder"
allowProxy="{Boolean}true"
categories="[dam.gui.coral.common.admin.timeline.events.workflow]"
dependencies="[granite.ui.coral.foundation]"/>
For the complete clientlib definition please refer to: https://bitbucket.org/3SHARE/aem-workflow-customisation/src/main/ui.apps/src/main/content/jcr_root/apps/dummy/clientlibs/clientlib-workflow/.
3|SHARE loves being part of the. Adobe developer community and we are happy to share solutions like the above with others. If you liked this one, reach out and let us know. And if you're looking for an outstanding team of architects and developers to help on your AEM project, get in touch!
Topics: Adobe Experience Manager, Development, AEM, Tech
Jorge Chércoles
Jorge Chércoles is an AEM Backend Developer at 3|SHARE. His passion is coding whether that is at work or outside of work. He loves that it gives him the ability to create things out of nothing. At 3|SHARE, Jorge enjoys the work environment and independence he has while doing his job. He is an Adobe Certified Expert having earned the Adobe Experience Manager Sites Developer certification.