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.
- 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);
<%@ 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);
}