One of the important aspects of workflow rule designing is to ensure that the rule instances finish after nicely when the work is done. Let’s take a simple scenario, a business want to send an email to the owner of an account if the account’s credit limit rises beyond a particular value.
A simple workflow rule waiting on the credit limit field would work.
Example:
When account is created
Wait
Account.Credit Limit > 10,000.00
End wait
E-mail To:[owner]; Subject: Account needs attention.
But this rule has an inherent flaw. What if the account never reached the specified limit and it was closed. The workflow rule will wait for ever, unless manually aborted.
To avoid this infinite wait we would need to add an OR condition to the wait clause to cease waiting when the account is deactivated. Unfortunately, CRM v3.0 workflow does not provide the facility to adding OR conditions to the wait clause.
Alternative solution out of this situation is to create a looping rule that loops every day and checks the account’s credit limit. This is an expensive and hacky solution. A neat solution to this issue would be the ability to abort all running or waiting rules when an account is deactivated.
This is what I will describe in the below section.
Approach: Create a custom .net assembly action that will abort all running wf process instances on the account when it is deactivated. Note that, we will have to take care that the rule calling this action should NOT kill itself. Also we should consider cases where the users accidentally deactivate the account and reactivate them after realizing. For this we can wait for a while before executing the rule.
Steps involved are:
1) Creating a .net assembly:
a. Create a sample project of name “CrmTest” whose output type is set to class library. I.e. it outputs a dll.
b. Add the web references to the project pointing to the crm server you have. The web reference url looks like: http://localhost/mscrmservices/2006/crmservice.asmx
c. Copy the below code in .cs file, compile the code and place the output dll into the assembly directory on the server.(C:\Program Files\Microsoft CRM\Server\bin\assembly)
d. When invoked, the code will query the crm server for all workflow process instances that are running on the passed in account id and will abort all except itself. To identify the calling rule we will use the rule name. I call this rule “AbortAll”
Code
using System;
using System.Text;
using System.Xml;
using CrmTest.localhost;
namespace CrmTest
{
public class Test
{
/// <summary>
/// Main method.
/// This is useful for testing your assembly.
/// </summary>
/// <param name=”args”>Arguments.</param>
public static void
{
string callerXml = “<caller><userid>EC087492-9A6C-DB11-B43E-0013720EC2DB</userid><merchantid>05C1328B-9A6C-DB11-B43E-0013720EC2DB</merchantid></caller>”;
Guid accountId = new Guid(“{5F5E22BD-9D6C-DB11-9E26-0013720EC2DB}”);
Test test = new Test();
test.AbortRunningWfInstances(callerXml, accountId, “AbortAll”);
}
/// <summary>
/// Aborts all running workflow instances, except the one identified by its rule name.
/// </summary>
/// <param name=”callerXml”>Caller Xml.</param>
/// <param name=”objectId”>Object Id of the entity.</param>
/// <param name=”ruleName”>Rule name of the calling rule.</param>
public void AbortRunningWfInstances(string callerXml, Guid objectId, string ruleName)
{
// Construct the query expression.
QueryExpression qe = new QueryExpression();
qe.ColumnSet = new AllColumns();
qe.EntityName = “wfprocessinstance”;
qe.Criteria = new FilterExpression();
qe.Criteria.Conditions = new ConditionExpression[2];
// First condition: objectid equals to the specified object.
qe.Criteria.Conditions[0] = new ConditionExpression();
qe.Criteria.Conditions[0].AttributeName = “objectid”;
qe.Criteria.Conditions[0].Operator = ConditionOperator.Equal;
qe.Criteria.Conditions[0].Values = new object[] { objectId };
// Second condition: statecode should indicate its a running instance.
qe.Criteria.Conditions[1] = new ConditionExpression();
qe.Criteria.Conditions[1].AttributeName = “statecode”;
qe.Criteria.Conditions[1].Operator = ConditionOperator.LessThan;
// 0-init, 1-active, 2-waiting, 3-paused, 4-completed, 5-aborted, 6-failed.
qe.Criteria.Conditions[1].Values = new object[] { 3 };
// Link to wfprocess entity to eliminate the calling rule instance.
qe.LinkEntities = new LinkEntity[1];
qe.LinkEntities[0] = new LinkEntity();
qe.LinkEntities[0].LinkFromEntityName = “wfprocessinstance”;
qe.LinkEntities[0].LinkFromAttributeName = “processid”;
qe.LinkEntities[0].LinkToEntityName = “wfprocess”;
qe.LinkEntities[0].LinkToAttributeName = “processid”;
qe.LinkEntities[0].LinkCriteria = new FilterExpression();
qe.LinkEntities[0].LinkCriteria.Conditions = new ConditionExpression[1];
qe.LinkEntities[0].LinkCriteria.Conditions[0] = new ConditionExpression();
qe.LinkEntities[0].LinkCriteria.Conditions[0].AttributeName = “name”;
qe.LinkEntities[0].LinkCriteria.Conditions[0].Operator = ConditionOperator.NotEqual;
qe.LinkEntities[0].LinkCriteria.Conditions[0].Values = new object[] { ruleName };
// Create crm service.
CrmService svc = new CrmService();
svc.Credentials = System.Net.CredentialCache.DefaultCredentials;
svc.Timeout = -1;
svc.CallerIdValue = new CallerId();
svc.CallerIdValue.CallerGuid = GetUserId(callerXml);
BusinessEntityCollection bec = (BusinessEntityCollection)svc.RetrieveMultiple(qe);
foreach(wfprocessinstance wfpi in bec.BusinessEntities)
{
SetStateWFProcessInstanceRequest request = new SetStateWFProcessInstanceRequest();
request.EntityId = wfpi.processinstanceid.Value;
request.WFProcessInstanceState = WFProcessInstanceState.Aborted;
request.WFProcessInstanceStatus = -1;
svc.Execute(request);
}
}
private Guid GetUserId(string callerXml)
{
//Note: the callerXml is of the fomat:
//callerXml = “<caller><userid>EC087492-9A6C-DB11-B43E-0013720EC2DB</userid><merchantid>05C1328B-9A6C-DB11-B43E-0013720EC2DB</merchantid></caller>”;
XmlDocument xmldoc = new XmlDocument();
xmldoc.LoadXml(callerXml);
return new Guid(xmldoc.DocumentElement.SelectSingleNode(“//caller/userid”).InnerText);
}
}
}
2) Registering the .net assembly with workflow.
a. Open the workflow.config file and add the following method node.
<method
name=”AbortAll”
assembly=”CrmTest.dll”
typename=”CrmTest.Test”
methodname=”AbortRunningWfInstances”
group=”Utility”>
<parameter name=”callerXml” datatype=”caller”/>
<parameter name=”objectId” datatype=”lookup” entityname=”account”/>
<parameter name=”ruleName” datatype=”string” default=”AbortAll”/>
</method>
Ensure that the allowunsignedassemblies=”true” is added to the workflow.config root node if your assembly is not signed
3) Constructing the workflow rule.
a. We will construct a workflow rule that will wait for 10 minutes when an account state change event is detected. It then checks the state of the account and if it is inactive, it will stop all running workflow rules on that account and add a note on the account about the action we took.
b. Create a workflow rule as shown below. For the objectId parameter of the custom .net assembly we pass the dynamic account value.
4) Try it out. J
Note:
1) Since this rule is has an ability to disrupt things. It will be good to make it inert to manual invocation by users. My other blog article on “Controlling scope of Apply rules” will help you in this.
2) To debug errors in workflow config file look into the application event log. Workflow service will log there, if any errors are found. Workflow service does not load the workflow config file at startup. It gets loaded on demand. To force loading of workflow config file, launch the wf manager.
3) In this example the rule runs on account entity. If you want to run the rule on a different entity then you will have to change the objectId parameter in the method registration lets say if you want to run it on contact. You will update the objectId parameter as <parameter name=”objectId” datatype=”lookup” entityname=”contact”/>. And if you want to run this rule for multiple entities, you will have to just register the method multiple times, one for each entity.