Introduction
Workflow Services API included in SharePoint 2013 allows you not only start and stop your workflows, but also to actually edit and create them, copy them between sites, export to WSP, associate to lists and many other things. This API is a part of SharePoint Client Object Model and thus accessible not only from C#, but also from JavaScript. Unfortunately, the API is not perfectly documented, and a bit cumbersome, and so it takes a while to figure out how to use it, and even more time - to write a working code. This article aims to provide ready-to-use code samples of the API usage (in JavaScript), so that a developer could concentrate on real business goals, not wasting time struggling through API thorns.
Background
SharePoint 2013 supports two types of workflows: old WF3 workflows (aka SharePoint 2010 Workflows) and new WF4 workflows (aka SharePoint 2013 Workflows). Workflow Service API is supposed to work primarily with the new WF4 workflows, but also contains some limited amount of methods for working with the old WF3 workflows.
New WF4 workflows are better in many ways, but also it's important to understand that they lack some features that the old workflows have. There are at least two important limitations concerning the WF4 workflows you might want to consider:
- Some workflow actions like e.g. "Set Approval Status" are not supported with the WF4 workflows, while at the same time they work fine in SP2010 workflows. See the full list on MSDN.
- SP2013 workflows can't be published globally as SP2010 workflows. E.g. if you want to create a workflow and use it on different sites of a site collection, you have to copy it to each site and manage it separately. Examples below can help automating this process.
There are other differences too (like e.g. SP2013 reusable workflows cannot be associated to a content type), but they're not so critical as the two I listed above.
The API itself consists of 4 services:
- WorkflowDeploymentService - provides methods for creating new and managing existing workflow definitions, e.g. modify, delete, publish, mark as deprecated, export to WSP, etc.
- WorkflowSubscriptionService - here you can find methods related to creating and managing workflow associations, i.e. attaching and detaching workflows to lists.
- WorkflowInstanceService - using this service, you can start, cancel, pause, resume and terminate workflows.
- InteropService - provides methods for calling old SP2010 workflows.
Examples
List of the examples:
- Enumerate workflow definitions of a site
- Copying a workflow between sites
- Copying a workflow between sites, customizing it on-fly
- Adding a workflow association (subscription)
- Starting a workflow
For enumerating workflow definitions of a site, enumerateDefinitions method of the WorkflowDeploymentService can be used.
Full code:
(function () {
var context = SP.ClientContext.get_current();
var web = context.get_web();
context.load(web);
var servicesManager = SP.WorkflowServices.WorkflowServicesManager.newObject(context, web);
context.load(servicesManager);
var workflowDefinitions = servicesManager.getWorkflowDeploymentService().enumerateDefinitions(false);
context.load(workflowDefinitions);
context.executeQueryAsync(function (sender, args) {
var definitionsEnum = workflowDefinitions.getEnumerator();
var empty = true;
console.log('Site ' + web.get_url() + ':');
while (definitionsEnum.moveNext()) {
var def = definitionsEnum.get_current();
console.log(def.get_displayName() + " (id: " + def.get_id() + ")");
empty = false;
}
if (empty) {
console.log('No 2013 workflows found.');
}
}, errFunc);
function errFunc(sender, args) {
alert("Error occured! " + args.get_message());
};
})();
Notice: old SharePoint 2010 workflows will not be returned by this code.
Here is the result of the execution of the code above on one of my test sites:
As you can see, it is possible to run this code even right from the console. In this case though, you have to make sure that "/layouts/15/SP.WorkflowServices.js" is included to the page.
This example shows how to copy SP2013 workflows between sites. Since you cannot publish SP2013 reusable workflows globally (this can only be done for SP2010 workflows), this code may be useful in many scenarios when you want to use same workflow throughout several sites.
The code below copies a workflow from a web to its subsites (NOT through all the sites in site collection, only one level down from the current site):
(function () {
var myDefinitionId = 'PUT-YOUR-GUID-HERE';
var context = SP.ClientContext.get_current();
var web = context.get_web();
var webs = web.get_webs();
context.load(web);
context.load(webs);
var servicesManager = SP.WorkflowServices.WorkflowServicesManager.newObject(context, web);
context.load(servicesManager);
var workflowDefinition = servicesManager.getWorkflowDeploymentService().getDefinition(myDefinitionId);
context.load(workflowDefinition);
context.executeQueryAsync(function (sender, args) {
workflowDefinition.set_displayName(workflowDefinition.get_displayName());
var websEnum = webs.getEnumerator();
while (websEnum.moveNext()) {
var subweb = websEnum.get_current();
saveWorkflowDefinition(context, subweb, workflowDefinition);
}
}, errFunc);
function errFunc(sender, args) {
alert("Error occured! " + args.get_message());
};
function saveWorkflowDefinition(context, web, myDefinition) {
var servicesManager = SP.WorkflowServices.WorkflowServicesManager.newObject(context, web);
context.load(servicesManager);
servicesManager.getWorkflowDeploymentService().saveDefinition(myDefinition);
servicesManager.getWorkflowDeploymentService().publishDefinition(myDefinition.get_id());
context.executeQueryAsync(function (sender, args) {
console.log('Definition saved to site ' + web.get_url());
}, errFunc);
}
})();
Notice: There is one gotcha with this code: after you retrieved a workflow definition and you are going to save it to some other site, you have to set display name to the same value, otherwise the definition will not have a name.
I ran this code, having a test workflow "Reusable workflow test" on the current web. The current web had two subsites (project1 and project2) where was no such workflow.
Result of execution:
After this, the workflow appears on subsites:
I double-checked: the workflow is correct and available for associating with lists:
Just in case, I associated this workflow to a list and started it, and it completed without problems.
Basically this is the same as in the previous example, with the only difference that the workflow is not copied "as is", but gets modified along the way.
In most cases, this can be achieved simply by modifying XAML code of the workflow, which is basically a piece of XML. So you can do a primitive string replace and that's it.
Consider you have a "Start a task process" action in your workflow, and you want to have different participants on the different sites where the workflow is deployed.
First of all, you should retrieve the XAML definition of the workflow (use workflowDefinition.get_xaml() property for this) and determine the place you need to change. In the example above, you would find something like this in your workflow:
<local:CompositeTask.AssignedTo>
<InArgument x:TypeArguments="sco:Collection(x:String)">
<local:ExpandInitFormUsers>
<local:ExpandInitFormUsers.Users>
<InArgument x:TypeArguments="sco:Collection(x:String)">
<p:BuildCollection x:TypeArguments="x:String"
Collection="{x:Null}" Result="{x:Null}">
<p:BuildCollection.Values>
<InArgument x:TypeArguments="x:String">i:0#.f|membership
|user@test.onmicrosoft.com</InArgument>
</p:BuildCollection.Values>
</p:BuildCollection>
</InArgument>
</local:ExpandInitFormUsers.Users>
</local:ExpandInitFormUsers>
</InArgument>
</local:CompositeTask.AssignedTo>
Obviously you need to replace the i:0#.f|membership|user@test.onmicrosoft.com
to something else, but limiting replace scope only to this fragment.
Now if you have a basic understanding of regular expressions, you can use this simple script to do it:
var regex = /(<local:CompositeTask.*?<local:CompositeTask.AssignedTo>.*?<InArgument x:TypeArguments="x:String">)([^\<]+)/;
definition.set_xaml(definition.get_xaml().replace(regex, "\\$1i:0#.f|membership|user2@test.onmicrosoft.com");
Admittedly, regular expressions are notorious for their problems with readability and maintainability. So if you're planning to do something really complicated with your workflow XAML, consider using Managed Client Object Model (i.e. C#) or at least some library like JsXML.
You may consider this example to be an addition to the Example 2: Copying a workflow between sites, since it is obvious that usually if you copy a workflow somewhere, you're likely going to attach it to a list right after that.
The code:
(function() {
var workflowDefinitionId = 'PUT-YOUR-GUID-HERE';
var listGuid = 'PUT-YOUR-GUID-HERE';
var web = context.get_web();
context.load(web);
var servicesManager = SP.WorkflowServices.WorkflowServicesManager.newObject(context, web);
context.load(servicesManager);
var historyList = web.get_lists().getByTitle('Workflow History');
context.load(historyList, 'Id');
var tasksList = web.get_lists().getByTitle('Tasks');
context.load(tasksList, 'Id');
context.executeQueryAsync(function (sender, args) {
var sub = new SP.WorkflowServices.WorkflowSubscription(context);
sub.set_name('Test subscription');
sub.set_enabled(true);
sub.set_definitionId(workflowDefinitionId);
sub.set_statusFieldName('WF status');
sub.set_eventSourceId(listGuid);
sub.set_eventTypes(["WorkflowStart"]);
sub.setProperty("TaskListId", tasksList.get_id().toString());
sub.setProperty("HistoryListId", historyList.get_id().toString());
sub.setProperty("FormData", "");
servicesManager.getSubscriptionService().publishSubscriptionForList(sub, listGuid);
context.executeQueryAsync(function (sender, args) {
console.log('Workflow association has been created successfully. Web: ' + web.get_url());
}, errFunc);
}, errFunc);
})();
Notice: the ugly thing about this code is that you should add three mandatory additional properties ("FormData", "TaskListId" and "HistoryListId") to the workflow subscription you create, otherwise even though the subscription will be created successfully, but the workflow will not work (it will fail right after it is started). These properties aren't documented anywhere, and it really took quite a while for me to figure this piece out.
For starting a SP2013 workflow, you should use startWorkflow method of the WorkflowInstanceService object.
Here is how you do it (this code starts the specified workflow on the first item of the specified list in the current web):
(function () {
var definitionId = 'PUT-YOUR-GUID-HERE';
var listGuid = 'PUT-YOUR-GUID-HERE';
var context = SP.ClientContext.get_current();
var web = context.get_web();
context.load(web);
var list = web.get_lists().getById(listGuid);
context.load(list);
var items = list.getItems(SP.CamlQuery.createAllItemsQuery());
context.load(items);
var servicesManager = SP.WorkflowServices.WorkflowServicesManager.newObject(context, web);
context.load(servicesManager);
var subs =
servicesManager.getWorkflowSubscriptionService().enumerateSubscriptionsByDefinition(definitionId);
context.load(subs);
context.executeQueryAsync(function (sender, args) {
if (resultsElement.innerHTML.indexOf('Loading') !== -1)
resultsElement.innerHTML = '';
var subsEnum = subs.getEnumerator();
while (subsEnum.moveNext()) {
var sub = subsEnum.get_current();
console.log('Web: ' + web.get_url() + ', Subscription: ' +
sub.get_name() + ', id: ' + sub.get_id());
var initiationParams = {};
servicesManager.getWorkflowInstanceService().startWorkflowOnListItem(
sub, items.getItemAtIndex(0).get_id(), initiationParams);
context.executeQueryAsync(function (sender, args) {
console.log('Workflow started.');
}, errFunc);
}
}, errFunc);
function (sender, args) {
alert("Error occured! " + args.get_message() +
'\r\nStack trace: ' + args.get_stackTrace());
}
})();
Notice: If you plan to operate in different webs, please make sure you use synchronized SP.ClientContext and SP.Web objects, otherwise the workflow will not start.
Conclusion
Workflow Services API in SharePoint 2013 is a powerful tool that can be used not only for general tasks like starting/stopping workflows, but for a wide range of maintenance and automation purposes, including e.g. solution upgrade scenarios, automated testing, workflow generation on-fly, etc. Taking into account growing importance of Apps and Client Object Model, it is particularly valuable for modern SharePoint development.
Please keep in mind that the API is not perfect and has it's own gotchas!
Good luck with your solutions!