Still being under impression from working with Azure Functions, I decided to try finally build a solution for Nintex Workflows for Office 365, where I have a single repository of workflows’ definitions, and from here I am able to publish them across the site, to different libraries or lists. Previously I was only able to do it using PowerShell or tools like Postman or Fiddler. However I wanted to have a single workflow that does all of the magic for me.

Nintex for Office 365 API

Unfortunately, the REST API that Nintex has in Office 365 is not so flexible and useful as it is when you work with Nintex on-premises versions. Find more yourself here. I focused myself on the action that is called “Import into an existing workflow” using which I can overwrite existing workflow with my source one. 

Prerequisites

To start even thinking of calling the Nintex REST API in Office 365 you need to be given your API Key. It is mandatory for the authentication purposes. You can get yours using the form here.

After you have the key, collect other, mandatory information:

  1. Login and Password of a user, whose context you will use.
  2. Base URL, which is built using the pattern: {customer}.nintexo365.com/api/{version}
    where:

    1. {customer} is simply name of your tenant.
    2. {version} – currently only v1 is available.
  3. GUID of the source workflow and the workflows you would like to overwrite.
  4. Libraries’ names where the target workflows are located.

That’s it. Easy? Not really. The only thing that cannot be automated here so far is obtaining the GUIDs. To get them, you have to export a workflow, open the “nwp” archive using 7ZIP for example, open the “Workflow” folder and then the “Metadata.xml” file. There you can find the workflow’s GUID:

NIntex Workflow GUID

Azure Function

The function does three things:

  1. Receives request data using POST request, where it gets all information required to get source workflow and publish it to target location;
  2. Gets the SPOIDCRL cookie for given credentials;
  3. Builds the function’s requests and executes it, so that it is able to overwrite the target workflow.

Function is written in C#, and looks like below:

#r "Microsoft.SharePoint.Client.dll"
#r "Microsoft.SharePoint.Client.Runtime.dll"
#r "Newtonsoft.Json"

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.SharePoint.Client;
using System.Security;
using System.Net.Http.Headers;
using System.Net.Http;
using System.Net;
using System.IO;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

// The API key and root URL for the REST API.
static private string apiKey = "APIKEY";
static private string apiRootUrl = "BASEURL";

// The SharePoint site and credentials to use with the REST API.
static private string spSiteUrl = "";
static private string spUsername = "USERLOGIN";
static private string spPassword = "USERPASS";

public static async Task<HttpResponseMessage> Run(HttpRequestMessage inputData, TraceWriter log)
{
    // Get request body
    dynamic data = await inputData.Content.ReadAsAsync<object>();
    string SourceWorkflowGUID = data?.SourceWorkflowGUID;
    string SourceWorkflowFileREST = data?.SourceWorkflowFileREST;
    string TenantName = data?.TenantName;
    spSiteUrl = data?.CurrentSiteURL; 

    string TargetWorkflowGUID = data?.TargetWorkflowGUID;
    string TargetListName = data?.TargetListName;

    // Create a new HTTP client and configure its base address.
    HttpClient Nintex_API_Client = new HttpClient();

    // Create a handler for HTTP Client for calling SP endpoints to create a cookie
    HttpClientHandler SP_API_Client_handler = new HttpClientHandler();
    SP_API_Client_handler.CookieContainer = new CookieContainer();

    // Get the SharePoint authorization cookie to be used by the HTTP client
    // for the request, and use it for the Authorization request header.
    string spoCookie = GetSPOCookie(log);
    
    if (spoCookie != String.Empty)
    {
        var authHeaderNintex = new AuthenticationHeaderValue(
            "cookie",
            String.Format("{0} {1}", spSiteUrl, spoCookie)
        );
        
        // Add the defined authentication header to the HTTP client's
        // default request headers.
        Nintex_API_Client.DefaultRequestHeaders.Authorization = authHeaderNintex;
        
        // Set auth cookie for SP HTTP Client
        SP_API_Client_handler.CookieContainer.Add(
          new Cookie("SPOIDCRL",
        spoCookie.TrimStart("SPOIDCRL=".ToCharArray()),
        String.Empty,
        new Uri(spSiteUrl).Authority));        
    }
    else
    {
        throw new InvalidOperationException("Cannot define Authentication header for request.");
    }

    HttpClient SP_API_Client = new HttpClient(SP_API_Client_handler);
    Nintex_API_Client.BaseAddress = new Uri(spSiteUrl);
    SP_API_Client.BaseAddress = new Uri(spSiteUrl);

    // Add common request headers for the REST API to the HTTP client.
    Nintex_API_Client.DefaultRequestHeaders.Accept.Add(
        new MediaTypeWithQualityHeaderValue("application/json"));
    Nintex_API_Client.DefaultRequestHeaders.Add("Api-Key", apiKey);

    // Add headers for the SP HTTP client.
    SP_API_Client.DefaultRequestHeaders.Accept.Add(
        new MediaTypeWithQualityHeaderValue("application/json"));    

    // Get contents of an exported workflow from the source list.
    // so that it can be used as a source 
    var workflowFileUri = String.Format("{0}/_api/web/getfilebyserverrelativeurl('{1}')/$value?binaryStringResponseBody=true",
        spSiteUrl.TrimEnd('/'),
        Uri.EscapeUriString(SourceWorkflowFileREST));
    
    HttpResponseMessage exportResponse = SP_API_Client.GetAsync(workflowFileUri).Result;
    
    // If we're successful, import the exported workflow to the destination list, as a new workflow.
    if (exportResponse.IsSuccessStatusCode)
    {
        // The response body contains a Base64-encoded binary string, which we'll
        // asynchronously retrieve as a byte array.
        byte[] exportFileContent = await exportResponse.Content.ReadAsByteArrayAsync();
          
        // Next, import the exported workflow to the destination list.
        var importWorkflowUri = String.Format("{0}/api/v1/workflows/packages/{1}?listTitle={2}",
            apiRootUrl.TrimEnd('/'),
            TargetWorkflowGUID,
            Uri.EscapeUriString(TargetListName));
        
        // Create a ByteArrayContent object to contain the byte array for the exported workflow.
        var importContent = new ByteArrayContent(exportFileContent);

        // Send a POST request to the REST resource.
        HttpResponseMessage importResponse = Nintex_API_Client.PutAsync(importWorkflowUri, importContent).Result;
        
        // Indicate to the console window the success or failure of the operation.
        if (importResponse.IsSuccessStatusCode)
        {
            var publishWorkflowUri = String.Format("{0}/api/v1/workflows/{1}/published",
            apiRootUrl.TrimEnd('/'),
            TargetWorkflowGUID);

            HttpResponseMessage publishResponse = Nintex_API_Client.PostAsync(publishWorkflowUri, importContent).Result;

            if (publishResponse.IsSuccessStatusCode)
            {
                return inputData.CreateResponse(HttpStatusCode.OK, "{\"StatusCode\":"+(int)publishResponse.StatusCode + ",\"ReasonPhrase\":\"" + publishResponse.ReasonPhrase + "\",\"Message\":\"Workflow imported and published\"}"); 
            }
            else 
            {
                return inputData.CreateResponse(HttpStatusCode.OK, "{\"StatusCode\":"+(int)publishResponse.StatusCode + ",\"ReasonPhrase\":\"" + publishResponse.ReasonPhrase + "\",\"Message\":\"Workflow imported but not published\"}"); 
            }

        }
        else
        {
            return inputData.CreateResponse(HttpStatusCode.BadRequest, "{\"StatusCode\":"+(int)importResponse.StatusCode + ",\"ReasonPhrase\":\"" + importResponse.ReasonPhrase + "\",\"Message\":\"Error importing and publishing a workflow\"}");
        }   
    }

    return inputData.CreateResponse(HttpStatusCode.BadRequest, "{\"StatusCode\":"+(int)exportResponse.StatusCode + ",\"ReasonPhrase\":\"" + exportResponse.ReasonPhrase + "\",\"Message\":\"Error getting source workflow contents\"}");

}

// Function for getting the SPOIDCRL cookie, needed for authentication
static private string GetSPOCookie(TraceWriter log)
{
    string result = String.Empty;
    try
    {
        // Construct a secure string from the provided password.
        var securePassword = new SecureString();
        foreach (char c in spPassword) { securePassword.AppendChar(c); }

        // Initialize a new SharePointOnlineCredentials object, using the 
        // specified username and password.
        var spoCredential = new SharePointOnlineCredentials(spUsername, securePassword);
        
        // If successful, try to authenticate the credentials for the specified site.
        if (spoCredential == null)
        {
            // Credentials could not be created.
            result = String.Empty;
        }
        else
        {
            // Credentials exist, so attempt to get the authentication cookie
            // from the specified site.
            result = spoCredential.GetAuthenticationCookie(new Uri(spSiteUrl));
        }
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.ToString());
        result = String.Empty;
    }

    
    return result;
}

You might be missing the SharePoint dlls file – I included them via references. Read here how to make references in Azure Function. The function is configured, to be triggered by POST request. The POST request Body is built as a JSON, the following way:

{
"SourceWorkflowGUID": "GUID",
"SourceWorkflowFileREST": "SITE RELATIVE URL TO THE SOURCE WORKFLOW NWP FILE",
"TargetWorkflowGUID": "GUID",
"TargetListName": "TARGET LIST NAME",
"TenantName": "TENANT NAME",
"CurrentSiteURL": "SITE URL"
}

Nintex Workflow & library settings

The source library has the following, custom columns:

  1. TargetWorkflowGUIDS (semicolon separated)
  2. TargetListNames (semicolon separated)
  3. SourceWokrflowGUID

The workflow is simple – it reads the metadata, split GUIDs using semicolon, then for each item in collection it constructs the JSON body and in the end it calls my Azure Function using the Web Request action.

   The URL to call your function is always built using the pattern: https://{function app name}.azurewebsites.net/api/{function name}

Nintex Workflow to call Azure Function

Final remarks

Although this solution works like a charm, there is one think that bothers me. Using Nintex web interface you are not allowed to publish or even save a second workflow in the same site, having a name that another workflow already has, However, when using REST API you can have a workflow, with the same name, published in multiple lists in the same site! I haven’t check what would happen if you then try to use the “Start workflow” action, but I suppose it will put workflow into “Suspended” mode having multiple workflows to choose from.

You can vote for the UserVoice suggestion, to define target workflow’s name when using Nintex Office 365 REST API methods: https://nintex.uservoice.com/forums/218291-3-nintex-workflow-for-office-365/suggestions/33048127-nintex-rest-api-should-allow-to-change-imported-wo