Wednesday, December 12, 2012

Quick review for Tridion 2013 Workflow


With the next SDL Tridion release we will receive several improvements regarding the way we envision and develop workflows. In this post I want to do a quick review and provide some guidance and samples about how to setup and configure a workflow in SDL Tridion 2013.

What is new?

There are several cool new features that make SDL Tridion 2013 workflow a more stronger and capable tool to develop content approval process and why not, BPM like processes... I will talk about that new feature later in this post.
  • Multiple items in a single process
Not as previous versions where the Subjects collection always contained one single item. In this version we will be able to start a workflow process for multiple items by just passing a collection of tcm uris. This feature will allow us to check out a set of items and process them in a single process.
There is not user interface for this feature so that we can just start a process instance for multiple items by using an API like Core services.
  • Bundled items in a single process
It is also cool to have the possibility to group items in a single business unit called a bundle. A bundle is a special type of virtual folder where content editors group items.
Using bundles has more advantages than just sending a collection of items because the system provides a GUI to manage bundles in both CME and Experience Manager. Additionally there are improvements related to security and process definition related to bundles like Bundle Management permissions and bundle specific activity definition options.
  • No items process
@Personal opinion: I love this new feature. SDL Tridion 2013 brings the concept of Task which is nothing different than a process with no items involved directly. I can define a task with several activities to do maintaining tasks or migration tasks or some BPM like tasks.
  • Native Core Services integration
Core services become the main API for workflows development. The new API brings a set of pre-defined core services variables that are available for workflow processing.
  • Process Instance State Management
If you are a Tridion Developer you may be familiar with Templating development and the concept of Packages. SDL Tridion 2013 comes with a similar way of state management called process variables where we can manipulate data that is available across a process instance.
  • Improved process suspend and resume
In previous releases it was hard to suspend and activity for a given amount of time. This new version comes with an improved mechanism for threads suspend, this means that activity suspend won't directly affect the amount of workflow threads available in the system. Additionally we have a time based resume mechanism.
  • Undo transactions
This functionality is not specific for workflows but it is a common used feature. Imagine that one of your tasks should rollback a previous Publish/UnPublish transaction; note that RollBack is not always the same as UnPublish. It is possible in the new version to rollback a previous transaction.

Implementing a 2013 Workflow

  • Process definition creation
A process definition is normally created by using the Visio Plug in 2013. The new Visio plug in has several improvements like bundle based settings and C# based automatic activities.


Note the new Constraints sections and the new Script Type. In this release we can develop Automatic Activities directly in C# without needing to register a .Net Class as a COM component.


  • Automatic Activities development
Core services are the preferred API to develop automatic activities in SDL Tridion 2013. This comes with a set of pre-defined variables that will give us access to the most important objects in a workflow process. In this post I will list the most important ones.
    • SessionAwareCoreServiceClient
This variable holds a session aware core service instance using the netTcp endpoint. This one maps to an ISessionAwareCoreService object.
    • CurrentActivityInstance
This variable holds an ActivityInstanceData object for the current activity.
    • ProcessInstance
This variable holds a ProcessInstanceData object for the current process instance.
    • ResumeBookmark
This variable holds a String object containing the bookmark to resume a suspended activity.

The following sample shows how a Workflow C# script will look like.

<%@ Assembly Name="System.ServiceModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"%>
<%@ Assembly Name="WorkflowTest, Version=1.0.0.0, Culture=neutral, PublicKeyToken=204ab1ccd7d1736e"%>

<%@ Import Namespace="System.ServiceModel"%>
<%@ Import Namespace="WorkflowTest"%>

WorkflowManager workflow = new WorkflowManager(SessionAwareCoreServiceClient);
workflow.PublishActivityHandler(CurrentActivityInstance, ProcessInstance, PublicationTargets.Dev, ResumeBookmark);

In the script above we can notice that we have a C# Fragments similar syntax, where you can use the pre-defined variables and also use custom assemblies that are registered in the GAC.




  • Custom Assembly Development

o    Publish Activity

/// <summary>
/// Publishes a Bundle to an specified Publication Target
/// </summary>
/// <param name="activityInstance"></param>
/// <param name="processInstance"></param>
/// <param name="target"></param>
public void PublishActivityHandler(ActivityInstanceData activityInstance, ProcessInstanceData processInstance, PublicationTargets target, string resumeBookmark) {
    if (string.IsNullOrEmpty(resumeBookmark)) {
        // Bundles are stored as Virtual Folders - Retrieve the bundle in the current Activity
        VirtualFolderData bundle = GetBundleForActivity(activityInstance);

        PublishInstructionData publishInstruction = new PublishInstructionData();
        publishInstruction.ResolveInstruction = new ResolveInstructionData();
        publishInstruction.RenderInstruction = new RenderInstructionData();
        publishInstruction.ResolveInstruction.IncludeWorkflow = true;

        // Retrieving the Publication Target
        string publicationTargetTitle = Enum.GetName(typeof(PublicationTargets), target);
        string publicationTargetId = GetPublicationTargetId(publicationTargetTitle);

        // Publish the bundle to the retrieved Publication Target
        string[] itemsToPublish = new string[] { bundle.Id };
        string[] targets = new string[] { publicationTargetId };
        PublishTransactionData[] publishTransactions = channel.Publish(itemsToPublish, publishInstruction, targets, PublishPriority.Normal, readOptions);

        // Store the Publish Transaction Id in the Process Instances Variables
        string publishTransactionKey = publicationTargetTitle + "PublishTransaction";
        if (processInstance.Variables.ContainsKey(publishTransactionKey)) {
            processInstance.Variables[publishTransactionKey] = publishTransactions[0].Id;
        }
        else {
            processInstance.Variables.Add(publishTransactionKey, publishTransactions[0].Id);
        }

        if (target == PublicationTargets.Live) {
            channel.SuspendActivity(activityInstance.Id, "Content Published to Live", DateTime.Now.Add(TimeSpan.FromMinutes(3)), "PublishLive", readOptions);
        }
        else {
            // Finish the Activity
            ActivityFinishData finishData = new ActivityFinishData() {
                Message = "Content published"
            };
            channel.FinishActivity(activityInstance.Id, finishData, readOptions);
        }
    }
    else if (resumeBookmark.Equals("PublishLive")) {
        // Finish the Activity
        ActivityFinishData finishData = new ActivityFinishData() {
            Message = "Content published"
        };
        channel.FinishActivity(activityInstance.Id, finishData, readOptions);
    }
}


o    Unpublish Activity

/// <summary>
/// Expires a Bundle from an specified Publication Target
/// </summary>
/// <param name="activityInstance"></param>
/// <param name="processInstance"></param>
/// <param name="target"></param>
/// <param name="resumeBookmark">Holds the ResumeBookmark predefined variable</param>
public void ExpireContentHandler(ActivityInstanceData activityInstance, ProcessInstanceData processInstance, PublicationTargets target, string resumeBookmark) {
    if (string.IsNullOrEmpty(resumeBookmark)) {
        // Bundles are stored as Virtual Folders
        VirtualFolderData bundle = GetBundleForActivity(activityInstance);

        UnPublishInstructionData unPublishInstruction = new UnPublishInstructionData();
        unPublishInstruction.ResolveInstruction = new ResolveInstructionData();

        // Retrieving the Publication Target
        string publicationTargetTitle = Enum.GetName(typeof(PublicationTargets), target);
        string publicationTargetId = GetPublicationTargetId(publicationTargetTitle);

        // Unpublish the bundle to the retrieved Publication Target
        string[] itemsToPublish = new string[] { bundle.Id };
        string[] targets = new string[] { publicationTargetId };
        PublishTransactionData[] publishTransactions = channel.UnPublish(itemsToPublish, unPublishInstruction, targets, PublishPriority.Normal, readOptions);

        // Store the Unpublish Transaction Id in the Process Instances Variables
        string unPublishTransactionKey = publicationTargetTitle + "UnpublishTransaction";
        if (processInstance.Variables.ContainsKey(unPublishTransactionKey)) {
            processInstance.Variables[unPublishTransactionKey] = publishTransactions[0].Id;
        }
        else {
            processInstance.Variables.Add(unPublishTransactionKey, publishTransactions[0].Id);
        }

        // Suspend the activity if expired in live
        if (target == PublicationTargets.Live) {
            channel.SuspendActivity(activityInstance.Id, "Expiration Gate (Cluth)", DateTime.Now.Add(clutch), "ExpireLive", readOptions);
        }
    }
    else {
        // Determines if the workflow should be finished otherwise it will Undo and finish
        if (resumeBookmark == "ExpireLive") {
            processInstance.Variables.Add("ClutchExpired", Boolean.TrueString);
        }

        // Finish the Activity
        ActivityFinishData finishData = new ActivityFinishData() {
            Message = "Content unpublished"
        };
        channel.FinishActivity(activityInstance.Id, finishData, readOptions);
    }
}

o    Reject Activity
/// <summary>
/// Rejects a Bundle, Undo the Publish Transaction and assign the Activity to its last performer
/// </summary>
/// <param name="processInstance"></param>
/// <param name="activityInstance"></param>
/// <param name="target"></param>
public void RejectPublishActivity(ActivityInstanceData activityInstance, ProcessInstanceData processInstance, PublicationTargets target) {
    // Get the last performer for previous Activity
    TrusteeData lastPerformer = GetLastPerformerForActivity(processInstance, "Create Or Edit Bundle");

    // Bundles are stored as Virtual Folders - Retrieve the bundle in the current Activity
    VirtualFolderData bundle = GetBundleForActivity(activityInstance);

    // Retrieving the Publication Target and Publish Transaction
    string publicationTargetTitle = Enum.GetName(typeof(PublicationTargets), target);
    string publishTransactionKey = publicationTargetTitle + "PublishTransaction";

    if (processInstance.Variables.ContainsKey(publishTransactionKey)) {
        string publishTransactionId = processInstance.Variables[publishTransactionKey];

        // Undo Publish Transaction
        channel.UndoPublishTransaction(publishTransactionId, QueueMessagePriority.Normal, readOptions);
    }

    // Finish the Activity
    ActivityFinishData finishData = new ActivityFinishData() {
        Message = "The bundle " + bundle.Title + " has been rejected and reassigned to " + lastPerformer.Title,
        NextAssignee = new LinkToTrusteeData() { IdRef = lastPerformer.Id }
    };
    channel.FinishActivity(activityInstance.Id, finishData, readOptions);
}