It has been a while since I last posted to this blog, for various reasons of course such as time and simply not being involved to much in SharePoint Development over the last years, or at least, not on the complex side of things 😉.
Now I have stumbled across another challenge where unfortunately I was not able to find much information about online, so decided to put this one online myself. I hope it will help someone who encounters the same issue.
Launching integrated workflow from host web
First off, what is an integrated workflow? Well, since a while, MS supports workflows being added through a SharePoint Add-In to be connected to a list on the host web, thus the workflow being integrated. These are usually Visual Studio Workflows, but could also be declaritive workflows though. That being said, they are based on the SP 2013 workflow platform.
Recently I wanted to launch such a workflow through scipt, because it should be launched on any item event, but instead, on the discretion of the user. Of course, he could navigate all the way to the workflow start screen, but in this scenario, I needed to have it started on a button click. Fortunately, the JSOM framework provides a workflow services manager which can kick those off for you. Sounds simple right? You have a list, which has workflows attached. You then just get all subscriptions, pick the one you want and launch it. Well, for SharePoint 2013 workflows (designer), that is indeed how it works. But for the integrated workflows, i.e. the ones which are attached through a SharePoint hosed app (or provider hosted for that matter), things are slightly different.
So, I will first start with how we would do it for SP 2013 workflows:
Add the following scripts tags to your page:
<script type="text/javascript" src="/_layouts/15/sp.js"> <script type="text/javascript" src="/_layouts/15/sp.core.js"> <script type="text/javascript" src="/_layouts/15/sp.runtime.js"> <script type="text/javascript" src="/_layouts/15/sp.workflowservices.js">
We can then lookup the subscription and start the workflow, assuming we know the subscriptionID.
If we do not, the workflowSubscriptionService can also return a collection which we can enumerate to find the one you want.
function startWorkflow(itemID, subID) {
var context = SP.ClientContext.get_current();
var web = context.get_web();
var wfServiceManager = SP.WorkflowServices.WorkflowServicesManager.newObject(context, web);
var subscription = wfServiceManager.getWorkflowSubscriptionService().getSubscription(subID);
context.load(subscription);
context.executeQueryAsync(function(sender, args) {
console.log(“Loaded subscription. Starting workflow.”);
var inputParameters = {};
wfServiceManager.getWorkflowInstanceService().startWorkflowOnListItem(subscription, itemID, inputParameters);
context.executeQueryAsync(function(sender, args) {
console.log(“Started workflow.”);
}, function(sender, args) {
console.log(“Cannot start workflow.”);
console.log(“Error: “ + args.get_message() + “\n” + args.get_stackTrace());
});
}, function(sender,args) {
console.log(“Cannot get subscription.”);
console.log(“Error: “ + args.get_message() + “\n” + args.get_stackTrace());
});
}
So pretty straight forward:
- Obtain the client context by getting ClientContext.get_current()
- Then get the services manager for the workflows.
- Obtain the subscription (association) though the manager passing the subscription ID.
- Finally, when found, start the workflow by calling startWorkflowForListItem on the workflow service, passing the subscription, the item ID of the list item and the initiation parameters
Ok, so far so good. But when I pass the subscription ID of an integrated workflow, that is, a workflow that is added through a SharePoint app, this will fail.
It will not be able to start the workflow as it cannot find that subscription.
The reason for this is as straightforward as frustrating to find. This is because the subscriptions are not defined in the Host web, but in the App web of the SharePoint app. Now it would be nice if MS also returned the integrated workflow subscriptions from the workflow services manager, but unfortunately it does not. So how do we go about it?
Well, instead of using the Host context, which we obtain by calling SP.ClientContext.get_current(), I use the context of the App web. The only problem is that the app web will be deployed to a different app web moving from one environment to another, so I needed to have something more stable.
The following is therefore how to obtain a context to the app web. The rest of the code can remain the same 😉, so let’s first look at how to get the app web context.
var appInstances = SP.AppCatalog.getAppInstances(clientContext, web);
clientContext.load(web);
clientContext.loadQuery(appInstances);
clientContext.executeQueryAsync( function () {
if (appInstances.get_count() > 0) {
for (var i = 0; 1 < appInstances.get_count() ; i++) {
var v = appInstances.getItemAtIndex(i);
if (v.get_title() == “your app name”) {
var url = v.get_appWebFullUrl();
} //if
} //for
} // if
} , function (sender, args) {
alert(JSON.stringify(args));
});
Please note that if your workflows are in a provider hosted app, the v.get_AppWebFullUrl will return null and instead, v.get_remoteAppUrl will return the url to the app.
So now we have the url of the App web in the url variable.
So we can use it to create a new ClientContext based on that url.
var context = new SP.ClientContext(url);
Now that we have that we can use the same lines of code as originally used, but using the different context instead.
So, putting it all together:
function StartWorkflow(itemId, workflowName) {
// get current context
var clientContext = new SP.ClientContext.get_current();var appInstances = SP.AppCatalog.getAppInstances(clientContext, web);
clientContext.load(web);
clientContext.loadQuery(appInstances);clientContext.executeQueryAsync(function () {
if (appInstances.get_count() > 0)
{
for (var i = 0; 1 < appInstances.get_count() ; i++)
{
var v = appInstances.getItemAtIndex(i);
if (v.get_title() == “your app name”) {
var url = v.get_appWebFullUrl();
StartAppWorkflow(itemId, workflowName, url);
}
}
}
}, function (sender, args) {
alert(JSON.stringify(args));
});
}function StartAppWorkflow(itemId, workflowName, appweburl) {
// setup context to the app web
context = new SP.ClientContext(appweburl);
factory = new SP.ProxyWebRequestExecutorFactory(appweburl);
context.set_webRequestExecutorFactory(factory);
var web = context.get_web();
context.load(web);
context.executeQueryAsync(function() {
wfsManager = SP.WorkflowServices.WorkflowServicesManager.newObject(context,web),
wfSubscriptions = wfsManager.getWorkflowSubscriptionService().enumerateSubscriptions();
context.load(wfSubscriptions);
wfsManager = SP.WorkflowServices.WorkflowServicesManager.newObject(context,web),
wfSubscriptions = wfsManager.getWorkflowSubscriptionService().enumerateSubscriptions();
context.load(wfSubscriptions);
context.executeQueryAsync(function () {
var wfsEnum = wfSubscriptions.getEnumerator();
while(wfsEnum.moveNext()) {
var wfSubscription = wfsEnum.get_current();
if(wfSubscription.get_name() == workflowName) {
wfsManager.getWorkflowInstanceService().startWorkflowOnListItem(wfSubscription,itemId,new Object());
context.executeQueryAsync(function() {
var note = SP.UI.Notify.addNotification(‘Started Workflow: ‘ + workflowName + ‘ on item: ‘ + itemId, false);
}, function() {
alert(‘Could not start workflow.’);
});
}
}
});
});
}
A special note needs to be made here. SharePoint 2016 is more secure and adheres to Cross Site Scripting rules (CORS). As the app web resides in a different domain than the host web, it will throw an exception violating CORS (401 unauthorized) when trying to execute against the app web context.
To circumvent, we install a proxy to execute the web requests, by calling:
factory = new SP.ProxyWebRequestExecutorFactory(appUrl);
context.set_webRequestExecutorFactory(factory);
This overrides the web request to take the one from the proxy.
That’s it! You should be able to start all integrated workflows now.