Intro 
In this post I’d like to share a workflow “attacher” implementation I built on a recent Sitecore XM Cloud project. The solution attaches workflows to new items based on a configurable list of template and path rules. It was fun to build and ended up involving a couple Sitecore development mechanisms I hadn’t used in a while:
- The venerable Sitecore configuration factory to declaratively define runtime objects
- The newer pipeline processor invoked when items are created from a template: addFromTemplate
This implementation provided our client with a semi-extensible way of attaching workflows to items without writing any additional code themselves. “But, Nick, Sitecore already supports attaching workflows to items, why write any custom code to do this?” Great question .
The Problem 
The go-to method of attaching workflows to new items in Sitecore is to set the workflow fields on Standard Values for the template(s) in question. For example, on a Page template in a headless site called Contoso (/sitecore/templates/Project/Contoso/Page/__Standard Values). This is documented in the Accelerate Cookbook for XM Cloud here. Each time a new page is created using that template, the workflow is associated to (and is usually started on) the new page.
Setting workflow Standard Values fields on site-specific or otherwise custom templates is one thing, but what about on out-of-the-box (OOTB) templates like media templates? On this particular project, there was a requirement to attach a custom workflow to any new versioned media items.
I didn’t want to edit Standard Values on any of the media templates that ship with Sitecore. However unlikely, those templates could change in a future Sitecore version. Also, worrying about configuring Sitecore to treat any new, custom media templates in the same way as the OOTB media templates just felt like a bridge too far.
I thought it would be better to “listen” for new media items being created and then check to see if a workflow should be attached to the new item or not. And, ideally, it would be configurable and would allow the client’s technical resources to enumerate one or more workflow “attachments,” each independently configurable to point to a specific workflow, one or more templates, and one or more paths.
The Solution 
Disclaimer: Okay, real talk for a second. Before I describe the solution, broadly speaking, developers should try to avoid customizing the XM Cloud content management (CM) instance altogether. This is briefly mentioned in the Accelerate Cookbook for XM Cloud here. The less custom code deployed to the CM the better; that means fewer points of failure, better performance, more expedient support ticket resolution, etc. As Robert Galanakis once wrote, “The fastest code is the code which does not run. The code easiest to maintain is the code that was never written.”
With that out of the way, in the real world of enterprise XM Cloud solutions, you may find yourself building customizations. In the case of this project, I didn’t want to commit to the added overhead and complexity of building out custom media templates, wiring them up in Sitecore, etc., so I instead built a configurable workflow attachment mechanism to allow technical resources to enumerate which workflows should start on which items based on the item’s template and some path filters.
addFromTemplate Pipeline Processor 
Assuming it’s enabled and not otherwise bypassed, the addFromTemplate pipeline processor is invoked when an item is created using a template, regardless of where or how the item was created. For example:
- When a new page is created in the Content Editor
- When a new data source item is created using Sitecore PowerShell Extensions
- When an item is created as the result of a branch template
- When a new media item is uploaded to the media library
- When several media items are uploaded to the media library at the same time
- …etc.
In years past, the item:added event handler may have been used in similar situations; however, it isn’t as robust and doesn’t fire as consistently given all the different ways an item can be created in Sitecore.
To implement an addFromTemplate pipeline processor, developers implement a class inheriting from AddFromTemplateProcessor (via Sitecore.Pipelines.ItemProvider.AddFromTemplate). Here’s the implementation for the workflow attacher:
using Contoso.Platform.Extensions; using Sitecore.Pipelines.ItemProvider.AddFromTemplate; ... namespace Contoso.Platform.Workflow { public class AddFromTemplateGenericWorkflowAttacher : AddFromTemplateProcessor { private List<WorkflowAttachment> WorkflowAttachments = new List<WorkflowAttachment>(); public void AddWorkflowAttachment(XmlNode node) { var attachment = new WorkflowAttachment(node); if (attachment != null) { WorkflowAttachments.Add(attachment); } } public override void Process(AddFromTemplateArgs args) { try { Assert.ArgumentNotNull(args, nameof(args)); if (args.Aborted || args.Destination.Database.Name != "master") { return; } // default to previously resolved item, if available Item newItem = args.ProcessorItem?.InnerItem; // use previously resolved item, if available if (newItem == null) { try { Assert.IsNotNull(args.FallbackProvider, "Fallback provider is null"); // use the "base case" (the default implementation) to create the item newItem = args.FallbackProvider.AddFromTemplate(args.ItemName, args.TemplateId, args.Destination, args.NewId); if (newItem == null) { return; } // set the newly created item as the result and downstream processor item args.ProcessorItem = args.Result = newItem; } catch (Exception ex) { Log.Error($"{nameof(AddFromTemplateGenericWorkflowAttacher)} failed. Removing partially created item, if it exists", ex, this); var item = args.Destination.Database.GetItem(args.NewId); item?.Delete(); throw; } } // iterate through the configured workflow attachments foreach (var workflowAttachment in WorkflowAttachments) { if (workflowAttachment.ShouldAttachToItem(newItem)) { AttachAndStartWorkflow(newItem, workflowAttachment.WorkflowId); // an item can only be in one workflow at a time break; } } } catch (Exception ex) { Log.Error($"There was a processing error in {nameof(AddFromTemplateGenericWorkflowAttacher)}.", ex, this); } } private void AttachAndStartWorkflow(Item item, string workflowId) { item.Editing.BeginEdit(); // set default workflow item.Fields[Sitecore.FieldIDs.DefaultWorkflow].Value = workflowId; // set workflow item.Fields[Sitecore.FieldIDs.Workflow].Value = workflowId; // start workflow var workflow = item.Database.WorkflowProvider.GetWorkflow(workflowId); workflow.Start(item); item.Editing.EndEdit(); } } }
Notes:
- The WorkflowAttachments member variable stores the list of workflow definitions (defined in configuration).
- The AddWorkflowAttachment() method is invoked by the Sitecore configuration factory to add items to the WorkflowAttachments list.
- Assuming the creation of the new item wasn’t aborted, the destination database is master, and the new item is not null, the processor iterates over the list of workflow attachments and, if the ShouldAttachToItem() extension method returns true, the AttachAndStartWorkflow() method is called.
- The AttachAndStartWorkflow() method associates the workflow to the new item and starts the workflow on the item.
- Only the first matching workflow attachment is considered—an item can only be in one (1) workflow at a time.
The implementation of the ShouldAttachToItem() extension method is as follows:
... namespace Contoso.Platform { public static class Extensions { ... public static bool ShouldAttachToItem(this WorkflowAttachment workflowAttachment, Item item) { if (item == null) return false; // check exclusion filters if (workflowAttachment.PathExclusionFilters.Any(exclusionFilter => item.Paths.FullPath.IndexOf(exclusionFilter, StringComparison.OrdinalIgnoreCase) > -1)) return false; // check inclusion filters if (workflowAttachment.PathFilters.Any() && !workflowAttachment.PathFilters.Any(includeFilter => item.Paths.FullPath.StartsWith(includeFilter, StringComparison.OrdinalIgnoreCase))) return false; var newItemTemplate = TemplateManager.GetTemplate(item); // check for template match or template inheritance return workflowAttachment.TemplateIds.Any(id => ID.TryParse(id, out ID templateId) && (templateId.Equals(item.TemplateID) || newItemTemplate.InheritsFrom(templateId))); } } ... }
Notes:
- This extension method determines if the workflow should be attached to the new item or not based on the criteria in the workflow attachment object.
- The method evaluates the path exclusion filters, path inclusion filters, and template ID matching or inheritance (in that order) to determine if the workflow should be attached to the item.
Here’s the WorkflowAttachment POCO that defines the workflow attachment object and facilitates the Sitecore configuration factory’s initialization of objects:
using Sitecore.Diagnostics; using System; using System.Collections.Generic; using System.Linq; using System.Xml; namespace Contoso.Platform.Workflow { public class WorkflowAttachment { public string WorkflowId { get; set; } public List<string> TemplateIds { get; set; } public List<string> PathFilters { get; set; } public List<string> PathExclusionFilters { get; set; } public WorkflowAttachment(XmlNode workflowAttachmentNode) { TemplateIds = new List<string>(); PathFilters = new List<string>(); PathExclusionFilters = new List<string>(); if (workflowAttachmentNode == null) throw new ArgumentNullException(nameof(workflowAttachmentNode), $"The workflow attachment configuration node is null; unable to create {nameof(WorkflowAttachment)} object."); // parse nodes foreach (XmlNode childNode in workflowAttachmentNode.ChildNodes) { if (childNode.NodeType != XmlNodeType.Comment) ParseNode(childNode); } // validate Assert.IsFalse(string.IsNullOrWhiteSpace(WorkflowId), $"{nameof(WorkflowId)} must not be null or whitespace."); Assert.IsTrue(TemplateIds.Any(), "The workflow attachment must enumerate at least one (1) template ID."); } private void ParseNode(XmlNode node) { switch (node.LocalName) { case "workflowId": WorkflowId = node.InnerText; break; case "templateIds": foreach (XmlNode childNode in node.ChildNodes) { if (childNode.NodeType != XmlNodeType.Comment) TemplateIds.Add(childNode.InnerText); } break; case "pathFilters": foreach (XmlNode childNode in node.ChildNodes) { if (childNode.NodeType != XmlNodeType.Comment) PathFilters.Add(childNode.InnerText); } break; case "pathExclusionFilters": foreach (XmlNode childNode in node.ChildNodes) { if (childNode.NodeType != XmlNodeType.Comment) PathExclusionFilters.Add(childNode.InnerText); } break; default: break; } } } }
Configuration 
The following patch configuration file is defined to A. wire-up the addFromTemplate pipeline processor and B. describe the various workflow attachments. In the sample file below, for brevity, there’s only one (1) attachment defined, but multiple attachments are supported.
<configuration> <sitecore> ... <pipelines> <group name="itemProvider" groupName="itemProvider"> <pipelines> <addFromTemplate> <processor type="Contoso.Platform.Workflow.AddFromTemplateGenericWorkflowAttacher, Contoso.Platform" mode="on"> <!-- Contoso Media Workflow attachment for versioned media items and media folders --> <workflowAttachmentDefinition hint="raw:AddWorkflowAttachment"> <workflowAttachment> <!-- /sitecore/system/Workflows/Contoso Media Workflow --> <workflowId>{88839366-409A-4E57-86A4-167150ED5559}</workflowId> <templateIds> <!-- /sitecore/templates/System/Media/Versioned/File --> <templateId>{611933AC-CE0C-4DDC-9683-F830232DB150}</templateId> <!-- /sitecore/templates/System/Media/Media folder --> <templateId>{FE5DD826-48C6-436D-B87A-7C4210C7413B}</templateId> </templateIds> <pathFilters> <!-- Contoso Media Library Folder --> <pathFilter>/sitecore/media library/Project/Contoso</pathFilter> </pathFilters> <pathExclusionFilters> <pathExclusionFilter>/sitecore/media library/System</pathExclusionFilter> <pathExclusionFilter>/Sitemap</pathExclusionFilter> <pathExclusionFilter>/Sitemaps</pathExclusionFilter> <pathExclusionFilter>/System</pathExclusionFilter> <pathExclusionFilter>/_System</pathExclusionFilter> </pathExclusionFilters> </workflowAttachment> </workflowAttachmentDefinition> ... </processor> </addFromTemplate> </pipelines> </group> </pipelines> ... </sitecore> </configuration>
Notes:
- N number of <workflowAttachmentDefinition> elements can be defined.
- Only one (1) <workflowId> should be defined per attachment.
- The IDs listed within the <templateIds> element are the templates the new item must either be based on or inherit from.
- The <pathFilters> element enumerates the paths under which the workflow attachment should apply. If the new item’s path is outside of any of the paths listed, then the workflow is not attached. This element can be omitted to forgo the path inclusion check.
- The <pathExclusionFilters> element enumerates the paths under which the workflow attachment should not apply. If the new item’s path contains any of these paths, then the workflow is not attached. This element can be omitted to forgo the path exclusion check. This filtering is useful to ignore new items under certain paths, e.g., under the Sitemap or Thumbnails media folders, both of which are media folders controlled by Sitecore.
Closing Thoughts 
While certainly not a one-size-fits-all solution, this approach was a good fit for this particular project considering the requirements and a general reticence for modifying Standard Values on OOTB Sitecore templates. Here are some pros and cons for this solution:
Pros
- Provides a semi-extensible, configuration-based way to start workflows on new items.
- Adding, updating, or removing a workflow attachment requires a configuration change but not code change.
- Allows for a template ID match or inheritance for more flexibility.
- Allows for path inclusion and exclusion filtering for more granular control over where in the content tree the workflow attachment should (or should not) apply.
Cons
- Deploying custom server-side code to the XM Cloud CM instance isn’t great.
- Arguably, creating custom templates inheriting from the OOTB templates in order to attach the workflows was the “more correct” play.
- A deployment to change a configuration file could still require a code deployment—many (most?) pipelines don’t separate the two. If configuration changes are deployed, then so is the code (which, of course, necessitates additional testing).
Takeaways:
- If you’re building an XM Cloud solution, do your best to avoid (or at least minimize) customizations to the CM.
- If you need to attach workflows to specific project templates or custom templates, do so via Standard Values (and serialize the changes)—don’t bother with custom C# code.
- If, for whatever reason, you need to resort to a custom solution, consider this one (or something like it).
- Of course, this solution can be improved; to list a few possible improvements:
- Pushing the configuration files into Sitecore to allow content authors to manage workflow attachment definitions. This would require a permissions pass and some governance to help prevent abuse and/or misconfigurations.
- Add support to conditionally start the workflow; at present, the workflow always starts on new items.
- Add logic to protect against workflow clobbering if, for whatever reason, the new item already has a workflow attached to it.
- Improve path matching when applying the path inclusion and exclusion filters.
- Logging improvements.
Thanks for the read!
Resources 
- https://sitecore.stackexchange.com/questions/523/execute-custom-logic-whenever-an-item-is-added-from-a-particular-branch-template
- https://sitecore-community.github.io/docs/documentation/Sitecore%20Fundamentals/Sitecore%20Configuration%20Factory
- https://developers.sitecore.com/learn/accelerate/xm-cloud
Source: Read MoreÂ