Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / productivity / SharePoint / SharePoint2013

Using SharePoint 2013 Workflow Services JS API

5.00/5 (10 votes)
20 Jun 2013CPOL6 min read 106.3K  
Code examples of using SharePoint 2013 Workflow Services API.

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:

  1. 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.
  2. 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:

  1. WorkflowDeploymentService - provides methods for creating new and managing existing workflow definitions, e.g. modify, delete, publish, mark as deprecated, export to WSP, etc.
  2. WorkflowSubscriptionService - here you can find methods related to creating and managing workflow associations, i.e. attaching and detaching workflows to lists.
  3. WorkflowInstanceService - using this service, you can start, cancel, pause, resume and terminate workflows.
  4. InteropService - provides methods for calling old SP2010 workflows.

Examples

List of the examples:

  1. Enumerate workflow definitions of a site
  2. Copying a workflow between sites
  3. Copying a workflow between sites, customizing it on-fly
  4. Adding a workflow association (subscription)
  5. Starting a workflow

Example 1: Enumerate workflow definitions of a site

For enumerating workflow definitions of a site, enumerateDefinitions method of the WorkflowDeploymentService can be used.

Full code:

JavaScript
(function () {

    // Getting client context and loading the current web
    var context = SP.ClientContext.get_current();
    var web = context.get_web();
    context.load(web);

    // Workflow Services API entry point - WorkflowServiceManager object
    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) {

        // enumerateDefinition returns ClientCollection object
        var definitionsEnum = workflowDefinitions.getEnumerator();

        var empty = true;

        console.log('Site ' + web.get_url() + ':');

        // Going through the definitions
        while (definitionsEnum.moveNext()) {

            var def = definitionsEnum.get_current();

            // Displaying information about this definition - DisplayName and Id
            console.log(def.get_displayName() + " (id: " + def.get_id() + ")");

            empty = false;

        }

        if (empty) {
           console.log('No 2013 workflows found.');
        }

    }, errFunc);


    // Error handling
    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:

Image 1

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.

Example 2: Copying workflows between sites

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):

JavaScript
(function () {

    // please, provide actual id of the workflow you're going to copy
    var myDefinitionId = 'PUT-YOUR-GUID-HERE';

    var context = SP.ClientContext.get_current();

    // get current web and it's subwebs
    var web = context.get_web();
    var webs = web.get_webs();
    context.load(web);
    context.load(webs);

    // get WorkflowServiceManager object for the root web
    var servicesManager = SP.WorkflowServices.WorkflowServicesManager.newObject(context, web);
    context.load(servicesManager);

    // load the workflow definition
    var workflowDefinition = servicesManager.getWorkflowDeploymentService().getDefinition(myDefinitionId);
    context.load(workflowDefinition);

    context.executeQueryAsync(function (sender, args) {


        // Looks meaningless, but without this "fix" you workflow
        // will not have the displayName and it's Id will appear
        // instead of the displayName
        workflowDefinition.set_displayName(workflowDefinition.get_displayName());

        // enumerate through subsites
        var websEnum = webs.getEnumerator();

        while (websEnum.moveNext()) {

            var subweb = websEnum.get_current();

            // save a copy of the workflow definition to the subsite
            saveWorkflowDefinition(context, subweb, workflowDefinition);

        }

    }, errFunc);


    function errFunc(sender, args) {
        alert("Error occured! " + args.get_message());
    };


    function saveWorkflowDefinition(context, web, myDefinition) {

        // we have to repeat the procedure of getting WorkflowServicesManager object because
        // it should be retrieved for each web separately
        var servicesManager = SP.WorkflowServices.WorkflowServicesManager.newObject(context, web);
        context.load(servicesManager);

        // Save and publish the definition
        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:

Image 2

After this, the workflow appears on subsites:

Image 3

I double-checked: the workflow is correct and available for associating with lists:

Image 4

Just in case, I associated this workflow to a list and started it, and it completed without problems.

Example 3: Copying a workflow between sites, customizing it on-fly

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:

XML
<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:

JavaScript
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.

Example 4: Adding a workflow association (subscription)

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:

JavaScript
(function() {

        // please, provide actual id of the workflow you're going to copy
        var workflowDefinitionId = 'PUT-YOUR-GUID-HERE';
        var listGuid = 'PUT-YOUR-GUID-HERE';

        var web = context.get_web();
        context.load(web);

        // Get WorkflowServicesManager object for the specified web
        var servicesManager = SP.WorkflowServices.WorkflowServicesManager.newObject(context, web);
        context.load(servicesManager);

        // Workflow History list
        var historyList = web.get_lists().getByTitle('Workflow History');
        context.load(historyList, 'Id');

        // Workflow Tasks list
        var tasksList = web.get_lists().getByTitle('Tasks');
        context.load(tasksList, 'Id');

        context.executeQueryAsync(function (sender, args) {

            // Creating the subscription
            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"]);

            // These 3 are MANDATORY! Otherwise the workflow will fail to complete
            sub.setProperty("TaskListId", tasksList.get_id().toString());
            sub.setProperty("HistoryListId", historyList.get_id().toString());
            sub.setProperty("FormData", "");

            // Associate the workflow with the list
            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.

Example 5: Starting a workflow

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):

JavaScript
(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!

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)