Introduction
This SharePoint 2010 Sequential Workflow project creates a very generic sequential workflow that can be customized through a XML. Working as a SP administrator/developer/supporter, for almost an year already, I came to conclusion that in most projects company assigned me to do the requirements regarding workflows are pretty much the same. There should be x number of tasks to complete, after each or some tasks are completed the item itself may have some sort of Status field updated to some defined value and after workflow is completed some kind of process can be executed. Each task generally should have at least 3 buttons - Approve, Reject, Change Request. Actually SharePoint already has a task form template with those buttons. So to create a new project for each workflow, which is 90+% similar to those we already have, is not a reasonable solution at least for me. A new project means to increase code maintenance, bigger knowledge transfer, bigger documentation, bigger drp, more steps (prone to errors) to recover or to reinstall the system. It is much easier to have 1 solution and a catalog of XML files that can be written by a non-technician.
To keep workflow simple and quick big item updates and background processes, that should be performed after the specific task is completed, can be delegated to Event Receivers. The workflow itself allows user to update the item, but updates are only executed once after user performed an action. One of those actions is workflow activation another one is task completion. So there shouldn't be conflicts with ER.
I understand that SP2010 is pretty much outdated by now but I hope to have the same project updated for SP2013 soon. Also I didn't think much about the definition of the XML I use to guide the workflow so all suggestions to improve it are welcome.
Description of the XML
So to automate our workflow through the XML we need 2 things: first - generic workflow that will just create the tasks and wait for user action to change those tasks; second - supply XML to that generic workflow to perform task actions. Since we are working with document management system XMLs can and will be stored in separate list in the same SP site. The project use the root SharePoint site to create a list with predefined name in. When you are going to create new workflow from this XML based workflow as template first you are going to create an XML file and upload it to this list. The XML file will be an attachment to the item with the same title as the workflow. So for instance if you need a Vacation Request Approval workflow you create an item with the title "Vacation Request Approval" in that list and attach XML with workflow "structure" to that item. Then you create a workflow in corresponding list, select this project as template, give it a name "Vacation Request Approval" and you are done.
The parent node is '<SPSolution type="Workflow">'. For now 'type' attribute is optional. Inside that goes '<Settings>' node which has all the settings. Inside that there are 3 different nodes for 3 different scopes of configuration: '<Modules>', '<Global>' and '<Context>'.
Modules store settings for 'modules' used in the workflow. The only module the workflow has now is mail function so all the mail settings are stored there. Module settings start with '<Module id="id_goes_here">' node. For e-mail is '<Module id="Mail">'. For instance you might add a function to store item or some data in the external data storage. So the configuration to access that storage could be stored in this node too. Perhaps '<Module id="MSSQL">'. Inside '<Module>' node go '<Option id="SETTING_ID">' nodes. In the case of mail function I made I only need those 7 nodes, but you can extend that to have more settings.
Global store settings for global usage. Inside Global node go '<Option>' nodes described above. I use these options to define logging parameters and couple of settings related to workflow itself, but they are not mandatory. Logging options could go inside 'Modules' too.
Context node store settings of the workflow tasks. It's structure is different from Modules and Global. That is because task information is far more complex than modules configuration. First there is 'Workflow/onInitiate/Item' node with repetitive 'Property' node that tells workflow what item fields to update when workflow is initiating. Property node has 'Id' attribute that identifies the field. Then goes Workflow/Terminate/Property node that tells workflow what field to check on to finish the workflow. The text of that node are ";" separated values that field can have to stop the workflow. I use the hidden field hStatus to finalize the workflow.
The most important nodes under the Context node are 'Tasks/Task' nodes. Each node of these defines 1 task. Each 'Task' node must have id attribute with integer 0 based value to define the id (TaskType) of the task. After 0 id can jump to 5, 20 or any other positive value, but 0 must exist to start the workflow. Task node has 3 children nodes: Properties, onUserAction and Next. Properties has repetitive Property nodes. Each Property node has id attribute which is required and few others which are not required like src, type and scope. "type" is used as a comment to tell user if the content of this node will be formatted. But workflow itself decides what content to format so type can be removed. The description of the properties goes below.
id="TITLE" - the title of the task. Formatted value.
id="ASSIGNED_TO" - the assignee of the task. ";" separated values. Each value can be SP group or user. Formatted value.
id="ASSIGNED_TO_TYPE" - the type of assignee. ";" separated values. 1 means user, 2 means group.
id="APPROVAL_TYPE" - the type of the task. 1 means single task, 2 means multi task -> created by replicator.
id="ACTION_PRIORITY" - the priority of task actions. ";" separated values. Mandatory with multi tasks. If your task form has 3 buttons that return result 1 for approved, 2 for rejected or 3 for change request and you assigned that task to 3 groups or people and they all clicked a different button then priority will tell which action to follow. If priority is 2;3;1 then first action to follow is rejected.
id="RECON_ACT" - the reconciliation action. Mandatory with multi tasks. If workflow can't decide what action was selected it will take reconciliation action and use it's settings to complete the task and create the next one.
id="WFH_COMMENT" - the log line in workflow history of the task. Formatted value.
id="AUTH_SUBJECT" - the subject of the e-mail to send to author of the item when task is created. Formatted value.
id="AUTH_BODY" - the body of the e-mail to send to author of the item when task is created. Formatted value.
id="ASSGTO_SUBJECT" - the subject of the e-mail to send to assignee of the task. Formatted value.
id="ASSGTO_BODY" - the body of the e-mail to send to assignee of the task Formatted value.
id="EMAIL_TO" - additional recipients of the e-mails. ";" separated e-mail addresses. Formatted value.
id="EMAIL_SUBJECT" - the subject of the additional e-mail to send when task is created. Formatted value.
id="EMAIL_BODY" - the body of the additional e-mail to send when task is created. Formatted value.
The rest of the properties in this node have scope="extended" which means these properties will become part of Task.ExtendedProperties and will be used in the task .xsn form.
onUserAction node defines each action's properties and updates. There are 3 results/actions defined in the task form used in this project - 1 (approve), 2 (reject) and 3 (change). onUserAction has repetitive Action node, one for each result. Action node has mandatory id attribute which defines action (1, 2, 3 in the case of this project) and children nodes Item (to update the item after task was completed), This (to update task data after task was completed), Mail (to send e-mail after task was completed) and WF_HIST (to write log into workflow history after task was completed). Each of 4 previously described nodes has Property nodes with id attribute and formatted content.
Next node defines what is the next task to create or if the workflow will be terminated. It has repetitive child node Action defined with attribute type that takes two values - "task" or "terminate". Inside that Action node there are 2 children nodes - If (for the action value) and Id for the next task id. Id 1000 is used actions with "type"=terminate.
Inside the project zip file there is a sample Test WF.xml. You can check that one to make a better notion about the XML structure.
If you checked Test WF.xml you probably saw there are some values that use the following notation {object.property}. Also in the description above I indicated that some or most values are Formatted values. Formatted values is a feature I implemented that allows you to extend even more the customization. There is a function in .cs files with the following declaration - private String sprintfSPItem(String templ, Hashtable ht, ref int err). That function takes 1st parameter templ and uses it as a format to build a returning String. {object.property} is a place holder that will be replaced with corresponding value. "object" part can be task, item, xml, local, winreg or system and "property" part can be some defined property or field (SPField) of that object. If SPField is SPFieldType.User then {object.property} becomes {object.user.property}. So for SPFieldType.User you can return e-mail, name or login. "local" means take values from the second parameter which is a Hashtable, "xml" means take values from Global node of configuration XML. "system" means return some calculated value like datetime, current user or new line. In case of current user again you can return any of 3 properties described above in item.SPFieldType.User property.
Create the project
Let's begin by opening VS2010 and creating a new project from the following template: Visual C#/SharePoint/2010/Sequential Workflow.
We are going to call our project WF_XMLBased and store it in our preferred location for VS projects. In the second screen we are going to specify the local site for debugging (though we are not going to debug, we will install release version directly to farm instead).
On the third screen the name of the workflow also will be WF_XMLBased and the type will be "List Workflow". On the fourth screen we will uncheck automatic association and press Finish. So far so good and this is how your project's structure should look like.
Now we are goung to rename our component Workflow1 to XMLBased and Workflow1.cs to XMLBased.cs.
After renaming file names we are going to refactor namespaces and class names. Double click on XMLBased.cs to open it in designer mode and press F7 to switch to code view. Change "namespace WF_XMLBased.Workflow1" to "namespace WF_XMLBased" (do the same thing in XMLBased.designer.cs). Now right click on Workflow1 and select Refactor/Rename. Type XMLBased in "New name" field (do the same thing in XMLBased.designer.cs).
Important thing - in the Elements,xml under XMLBased change CodeBesideClass from WF_XMLBased.Workflow1.Workflow1 to WF_XMLBased.XMLBased.
Create custom activity
Since our workflow has to support multi tasks we need to create custom activity that will be replicated when those multi tasks are created. Let's create that custom activity first. In the Solution Explorer right click on the project name (WF_XMLBased) and then select Add/New Item... Select the template Visual C#/General/Component Class. Name the new component ReplicatedTaskActivity. After creating the component it will show the error - "Error loading workflow", but there is nothing to worry about we will fix this error right away. Open ReplicatedTaskActivity.Designer.cs in the code editor and remove this declaration "private System.ComponentModel.IContainer components = null;" and this function "protected override void Dispose(bool disposing)" along with their comments. So now "#region Component Designer generated code" is the first line in the "partial class ReplicatedTaskActivity". Also remove "components = new System.ComponentModel.Container();" from "InitializeComponent" function.
Now open ReplicatedTaskActivity.cs, the one with error message, in code editor. Remove the copy constructor "public ReplicatedTaskActivity(IContainer container)" and inherit ReplicatedTaskActivity from SequenceActivity.
By switching back to the designer mode you can see that error message is gone.
Drag following activities into the ReplicatedTaskActivity area (Drop Activities Here): CreateTask, While, onTaskChanged (inside While) and UpdateTask. I'm not sure if there are some kind of standards for activities naming, but if there are I'm not following them. So here are the names I gave to activities and their properties:
CreateTask - crtRepTask
CorrelationToken - tokRepTask (owner activity ReplicatedTaskActivity)
MethodInvoking - RepTask_Creating
TaskId - RepTask_TI1
TaskProperties - RepTask_TP1
While - whlRepTask
Condition - Code Condition
Method - RepTask_Continue
OnTaskChanged - otcRepTask
CorrelationToken - tokRepTask (owner activity ReplicatedTaskActivity)
Invoked - RepTask_Changed
AfterProperties - RepTask_AP1
BeforeProperties - RepTask_BP1
TaskId - RepTask_TI1
UpdateTask - updRepTask
CorrelationToken - tokRepTask (owner activity ReplicatedTaskActivity)
MethodInvoking - RepTask_Updating
TaskId - RepTask_TI1
TaskProperties - RepTask_TP1
Now we are going to add more activity properties and going to enclose them into "#region Activity Declarations". Plus the methods we defined before will be enclosed into "#region Activity Implementations". Also there are some helper functions we adding related to logging, SP and general utilities. I'm not going to describe them 1 by 1, but only 2 or 3 I find particularly important for the functionallity of the workflow. You can find all the functions in the source code attached to this article.
String result = RepTask_AP1.ExtendedProperties["Result"].ToString();
if (TaskR2Os.ContainsKey(result))
{
STOutcome = ((Hashtable)TaskR2Os[result])["Outcome"].ToString();
String task_ids = getWFItemFieldData("hParallelTasks");
String[] tids = task_ids.Split(';');
for (int x = 0; x < tids.Length; x++)
{
if (tids[x] == RepTask_TI1.ToString())
{
tids[x] += ":" + result;
}
}
task_ids = String.Join(";", tids);
List<string> f2v = new List<string>();
f2v.Add("hParallelTasks");
f2v.Add("text");
f2v.Add(task_ids);
updateSPListItem(f2v);
foreach (DictionaryEntry entry in (Hashtable)TaskR2Os[result])
{
if (!entry.Key.ToString().ToLower().Equals("outcome"))
{
RepTask_TP1.ExtendedProperties[entry.Key.ToString()] = entry.Value.ToString();
}
}
}
</string></string>
The outcome of each replicated task is stored in the item's field hParallelTasks. The "hard" part goes in Replicator Completed method. We will see the slice of that function below. The rest of RepTask_Updating is workflow history writning.
One important thing to have in mind when we develop workflows programatically is to have a Fault Handler enabled and logging the errors. It will save you hours of researching and debugging. In design mode right click on ReplicatedTaskActivity and select View Fault Handlers.
Drag one Fault Handler into the faultHandlersActivity1. As FaultType select Referenced Assemblies/mscorelib/System/Exception (System.Exception to capture all the errors). Now drag Code activity into the faultHandlerActivity1 and name it errReportingActivity1. Type errReportingActivity_Log in the ExecuteCode property.
private void errReportingActivity_Log(object sender, EventArgs e)
{
FaultHandlerActivity faultHandler = ((Activity)sender).Parent as FaultHandlerActivity;
log_err("Fatal error caught in RTA - " + faultHandler.Fault.Message + "; stack trace is " + faultHandler.Fault.StackTrace);
}
After you copied all the functions and properties to the ReplicatedTaskActivity.cs build the project, install bin/Release/WF_XMLBased.dll to GAC (drag bin/Release/WF_XMLBased.dll to Windows/assembly folder) and reopen the project.
Create workflow
After you reopened the project open XMLBased.cs in design mode. This far you only should have onWorkflowActivated1 activity on the drawn on the working area. Drag the following activities in the workflow area: While, Sequence (inside While), Code (inside Sequence), If (under Code inside Sequence). Under the "true"/left branch drag the following activities: Replicator, ReplicatedTaskActivity (inside Replicator). You should see that custom activity under WF_XMLBased components category. Under the "false"/right branch of the if drag the following activities: Sequence, CreateTask (inside Sequence), While (inside Sequence), OnTaskChanged (inside While), UpdateTask (inside Sequence). This is how your design screen should be looking now.
Here is how I named all the activities and their properties and methods:
OnWorkflowActivated - owaMain
CorrelationToken - tokActivation (owner XMLBased)
Invoked - WFMain_Activated
WorkflowProperties - workflowProperties
While (after owaMain) - whlMain
Condition - Code Condition
Method - WF_Continue
Sequential (after whlMain) - seqMain
Code - codMain
ExecuteCode - Prepare_Task
If (after codMain) - ifeMainFork
Left path - ifeMainForkTrue
Condition - Code Condition
Method - Is_Multiple_Task
Replicator - repMain
ChildInitialized - ChildInitialized_Invoked
Completed - Completed_Invoked
Initialized - Initialized_Invoked
ExecutionType - Parallel
InitialChildData - assignees (public IList assignees = default(System.Collections.IList);)
ReplicatedTaskActivity - replicatedTaskActivity
Right path - ifeMainForkFalse
Sequence - seqMainTask
CreateTask - crtMainTask
CorrelationToken - tokMainTask (owner activity seqMainTask)
MethodInvoking - MainTask_Creating
TaskId - MainTask_TI1
TaskProperties - MainTask_TP1
While - whlMainTask
Condition - Code Condition
Method - MainTask_Continue
OnTaskChanged - otcRepTask
CorrelationToken - tokMainTask (owner activity seqMainTask)
Invoked - MainTask_Changed
AfterProperties - MainTask_AP1
BeforeProperties - MainTask_BP1
TaskId - MainTask_TI1
UpdateTask - updMainTask
CorrelationToken - tokMainTask (owner activity seqMainTask)
MethodInvoking - MainTask_updating
TaskId - MainTask_TI1
TaskProperties - MainTask_TP1
If, after you changed the names of activities, some errors regarding lost objects and bad references persist you can open XMLBased.designer.cs and change those old names manually. For example after I changed Workflow1 to XMLBased OnWorkflowActivated was complaining about not being able to locate Workflow1. Changing XMLBased.designer.cs fixed it for me.
Now let's check a slice of Completed_Invoked method of repMain
String task_ids = getWFItemFieldData("hParallelTasks");
if (task_ids.Equals("") || task_ids.Equals(String.Empty))
{
log_errTerminateWF("Undefined parallel tasks");
return;
}
String[] tids = task_ids.Split(';');
Hashtable ress = new Hashtable();
String uni_res = "";
for (int x = 0; x < tids.Length; x++)
{
String[] task = tids[x].Split(':');
String tid = task[0].ToString();
String res = task[1].ToString();
uni_res = res;
if (!ress.ContainsKey(res))
{
ress.Add(res, 1);
}
else
{
ress[res] = Convert.ToInt32(ress[res].ToString()) + 1;
}
}
String curr_task = getWFItemFieldData("hCAT");
if (curr_task.Equals(""))
{
log_errTerminateWF("Undefined current task");
return;
}
if (!ctxConfig.Contains(curr_task))
{
log_errTerminateWF("Undefined config in task " + curr_task);
return;
}
Hashtable ht = (Hashtable)ctxConfig[curr_task];
if (!ht.Contains("props"))
{
log_errTerminateWF("Undefined properties in task " + curr_task);
return;
}
if (!ht.Contains("onusract"))
{
log_errTerminateWF("Undefined user actions in task " + curr_task);
return;
}
if (!((Hashtable)ht["props"]).Contains("ACTION_PRIORITY"))
{
log_errTerminateWF("'Action priority' is not defined in task " + curr_task);
return;
}
if (!((Hashtable)ht["props"]).Contains("RECON_ACT"))
{
log_errTerminateWF("'Reconciliation act' is not defined in task " + curr_task);
return;
}
String[] act_pri = ((Hashtable)ht["props"])["ACTION_PRIORITY"].ToString().Split(';');
String rec_tsk = ((Hashtable)ht["props"])["RECON_ACT"].ToString();
String oua = "";
if (ress.Count == 1)
{
oua = uni_res;
}
else if (sameTaskResults(ress))
{
for (int x = 0; x < act_pri.Length; x++)
{
if (ress.ContainsKey(act_pri[x]))
{
oua = act_pri[x];
break;
}
}
}
Boolean send_email = false;
if (oua.Equals(""))
{
oua = rec_tsk;
send_email = true;
}
if (!((Hashtable)ht["onusract"]).Contains(oua))
{
log_errTerminateWF("Undefined user action " + oua + " in task " + curr_task);
return;
}
The content of hParallelTasks is parsed and the Hashtable ress is populated with outcomes as keys and outcomes quantities as values. The logic to choose the next task is simple: if ress has only 1 key then the next task corresponds to that outcome or if all the outcomes has the same quantities then ACTION_PRIORITY tells the next task or if none of the previous use RECON_ACT.
Create custom task form and add it to the project
I'm not going to cover the well known and documented topic as InfoPath Designer in this article. We suppose you alredy designed the form or simply use the one included in this project. Most probably you already know how to add .xsn to the workflow, but let's check it anyway. So right click on XMLBased, select Add/New Item and then select template SharePoint/Module. Type Forms as module name and click Add. Then right click on Forms select Add/Existing Item, navigate to published .xsn form and Add it. In this project I use form named "ApprRejChangeForm.xsn". Now open XMLBased/Forms/Elements.xml and remove or simply comment out this line "<File Path="Forms\Sample.txt" Url="Forms/Sample.txt" />". Now open XMLBased/Elements.xml and add the following lines:
TaskListContentTypeId="0x01080100C9C9515DE4E24001905074F980F93160" as an attribute of "<Workflow".
After "<StatusPageUrl>_layouts/WrkStat.aspx</StatusPageUrl>" add
"<TaskN_FormURN>URN_OF_YOUR_XSN</TaskN_FormURN>" with N ranging from 0 to some integer possitive number like 40. In this project <TaskN_FormURN> goes up to <Task39_FormURN>.
Now double click Feature1 and move Forms (WF_XMLBased) to the Items in the Feature (right side). This is how your feature window should look like:
Create feature event receiver
In the beginning I mentioned that XML files that define a workflow tasks will be stored in the list in the same or parent site. So we need to create that list on some event that happens during the installation of .wsp. To achieve that we are going to need feature event receiver. Right click on the Feature1 and select Add Event Receiver. Custom Event Receiver will override the default installation steps so along with creating the list we will have to register .xsn form. To register the .xsn files you will have to add a reference to your project. In the Solution Explorer right click on the References and select Add Reference. In the dialog box select Browse tab and got to C:\Program Files\Microsoft Office Servers\14.0\Bin\. Select Microsoft.Office.InfoPath.Server.dll and click Ok. If you are using .Net 3.5 it might display a warning message about compatibility issues. You can ignore that and click Yes.
Uncomment 2 functions in Feature1.EventReceiver.cs: FeatureInstalled and FeatureUninstalling.
public override void FeatureInstalled(SPFeatureReceiverProperties properties)
{
base.FeatureInstalled(properties);
FormsService formsService = GetFormsService();
if (formsService == null)
{
log_err("Unable to retrieve FormsService during installation of \"" + properties.Feature.Definition.Name + "\". Argument formsService was null.");
}
else
{
List<string> formTemplates = GetInfoPathFormTemplates(properties.Definition.Properties, properties.Definition.RootDirectory);
foreach (string formTemplate in formTemplates)
{
if (formsService.FormTemplates.ItemFromFile(formTemplate) == null)
{
FormTemplateCollection.RegisterFormTemplate(formTemplate, properties.Definition, false);
}
}
}
SPSite site = null;
SPWeb web = null;
int err = 0;
try
{
site = properties.UserCodeSite != null ? properties.UserCodeSite : new SPSite(site_url);
}
catch (Exception ex)
{
log_err("Can't initiate the site from Feature ER; details - " + ex.Message + "\r\n" + ex.StackTrace);
err = 1;
}
if (err == 0)
{
try
{
web = site.OpenWeb();
}
catch (Exception ex)
{
log_err("Can't open the web; details - " + ex.Message + "\r\n" + ex.StackTrace);
err = 2;
}
}
if (err == 0)
{
try
{
SPListCollection lists = web.Lists;
Boolean found = false;
for (int x = 0; x < lists.Count; x++)
{
if (lists[x].Title.Equals(cfg_list))
{
found = true;
}
}
if (!found)
{
lists.Add(cfg_list, cfg_list_desc, SPListTemplateType.CustomGrid);
}
}
catch (Exception ex)
{
log_err("Can't create the list; details - " + ex.Message + "\r\n" + ex.StackTrace);
err = 2;
}
}
}
</string>
public override void FeatureUninstalling(SPFeatureReceiverProperties properties)
{
base.FeatureUninstalling(properties);
FormsService formsService = GetFormsService();
if (formsService == null)
{
log_err("Unable to retrieve FormsService during installation of \"" + properties.Feature.Definition.Name + "\". Argument formsService was null.");
}
else
{
List<string> formTemplates = GetInfoPathFormTemplates(properties.Definition.Properties, properties.Definition.RootDirectory);
foreach (string formTemplate in formTemplates)
{
if (formsService.FormTemplates.ItemFromFile(formTemplate) != null)
{
formsService.FormTemplates.UnregisterFormTemplate(formTemplate, properties.Definition);
}
}
}
}
</string>
The code of these methods is pretty much straight forward. In the FeatureInstalled we first register custom task form then create the list. In the FeatureUninstalling we just unregister form. We don't delete the list. Also there is a property in "Feature1EventReceiver" class named "site_url". You have to setup that one to the site you want to deploy your workflow on (parent site). For exmaple for https;//localhost/legal/ the site_url would be just https://localhost/.
Modification workflow does to item structure
To know next task to create, store consolidated comments and other data required to run this solution properly workflow verifies the existence and creates, if necessary, various hidden fields in the item. Let's quickly review those fields.
private Hashtable hdn_flds = new Hashtable()
{
{"hCAT", "singleline_text"},
{"hNXT", "singleline_text"},
{"hPVT", "singleline_text"},
{"hWFUPD", "singleline_text"},
{"hStatus", "choice|In Progress|Completed|Canceled|Rejected"},
{"hWFXMLContext", "multiline_text"},
{"hWFXMLGlobal", "multiline_text"},
{"hWFXMLModules", "multiline_text"},
{"hXMLTskHist", "multiline_text"},
{"hParallelTasks", "multiline_text"},
{"hConsolidatedComments", "multiline_text"}
};
hCAT - stores the id (TaskType) of the current task.
hNXT - stores the id of the next task.
hPVT - stores the id of the previous task (is not used in code).
hWFUPD - when item is being updated by the workflow this field is set to 1. So if the list also has event receiver listening updates it can check if the update comes from the workflow.
hStatus - field used to check if the workflow should continue or finish.
hWFXMLContext, hWFXMLGlobal, hWFXMLModules - the content of XML file is sliced in 3 parts. Each part is stored in 1 of these fields.
hXMLTskHist - will store the GUIDs of the task. Is not populated or used yet.
hParallelTasks - will store the GUIDs and outcomes of the mult tasks.
hConsolidatedComments - stores consolidated comments.
Using the code
You can create your own solution and then add files or functions from the attached project. Or you can simply use the attached one. After building you can deploy the solution using VS2010 or SharePoint PowerShell commands. If you are using the attached solution you must setup the site you want to deploy the workflow on.
The attached solution has some non-critical issues which I hope to fix soon. Those issues shouldn't be an impediment to run the workflow.
History
Version 1.0.