I recently had a client who needed a workflow that could handle voting in a way that was a bit challenging to get working. I'm sure there a multiple ways to handle this, and I tried many unsuccessfully. I wanted to share an approach that I was able to apply successfully in case anyone else has a similar scenario. At a very high level, they need to gather votes and count the different responses, but the number of people who would actually respond might vary.
The business requirements were as follows:
- Allow for email reply (LazyApproval)
- Send reminders
- Auto-close the task(s) after a specified time
- Allow everyone to vote
- Tally the votes, including the number of "no response", and save back to the item
I'll go through the requirements individual and describe what I used to address them first.
Allow for email reply
This one was easy, enable LazyApproval in whatever task action I would use.
Don't just put a number in there...
This was one of the requirements that drove the decision to use Flexi tasks. Initially, during the design of the workflow, I put "5" into the "Number of reminders" field arbitrarily.
This is no good... don't do it this way.
Something unexpected happened with the escalations when I did this and it took me a while to put the pieces together. Now, it's possible that I got this wrong, but it seemed like the items wouldn't do the escalation (Complete task) until after all of the reminders were sent. I was not expecting a dependency there, and maybe it is a bug in the version I have or I just got it wrong, but everything I observed made me think this is how it was functioning. If my item was set with a time to escalation that was 1 day away, but I had the 5 reminders above, it wouldn't complete the task until after all of the reminders were sent. So, with that in mind, I did the following:
The right way
First, I will get the number of days (whole days, not partial) between the current date/time and the due date/time. I started with a Build string action, so I could take advantage of the inline functions like fn-DateDiffDays to get the number of days until the due date and time.
Next, I'll split the result of the DateDiffDays function to get rid of any decimals. The reason for this here is that I will be using the number for how many reminders to send, so I just need the whole number. Then, to get the number of minutes until the task auto closes, I will take the total number of minutes until it is due minus the number of minutes it was sending reminders. There is a dependency here, and if you don't subtract the time it was sending reminders, your tasks will not close until all reminders have been sent AND the amount of escalation time has passed. I didn't realize this at first, and I had a lot of tasks that were not closing automatically. They would have 5 days until due for example, and send out 5 reminders... but the escalation timer didn't start until all of the reminders were sent!
Anyway, on to the whole number of days... Split the variable. Make sure to use the escape character "\" since the decimal is a special character for RegEx. This was one of many lessons I learned during the development of this workflow. Now, you will have the number saved into a collection variable. If the result had been 2.3 days, the collection would now have 2 at index 0 and 3 at index 1.
Next, the Collection Action to get just the whole number at index 0. I am referencing a variable "numCollIndex" which is only used to grab index 0 of this collection. It has a default value of 0.
Here, the result is referenced in the Number of reminders field for the Flexi task.
This gave me the variable number of reminders without rounding, so if the due date is 1.2 days away, I get one reminder, if it is 2.8 days away, I get 2 reminders.
Auto-close the tasks after a specified time (escalation)
This was a requirement that pointed me to Flexi tasks. The due date and time for the voting on these items could be specific, like in the middle of the day, so I couldn't just use days for this one.
We'll need a little math. While we're waiting for all reminders to finish, we need to know the total number of minutes that will elapse. We can use the whole number of days x 1440 to get the number of minutes (the variable is numReminderMinutes). This will give us part of the equation we need to accurately determine how long to set for the task auto-escalation.
The next step is to get the total number of minutes until the due date/time.
Using a build string, you can get this number using the DateDiffMinutes function. Now we have the minutes until the due date/time, but it is a text variable.
Convert the value to a number with a Convert Value action. The variable is numTotalMinutesUntilDue.
With a math operation, take the total minutes until due - number of minutes elapsed while waiting for reminders to get the number of escalation minutes. This is how long the tasks wait after all reminders have gone out. Once this is done, the auto close occurs. This is saved in NumRBEscalationMinutes. We'll put that into the Flexi-tasks later.
Here is the Flexi task with that variable for the Time to escalation:
I added an outcome and comments text to more easily track who didn't respond in the workflow tasks list. Also, I need it for the tallying part later.
The Flexi tasks have an "Other" branch to handle the "No Response" scenario when the voting window has closed.
Allow everyone to vote
The "Request approval" action has a vote option, but unfortunately, it doesn't have the reminders or the escalation options, so this became a challenge and a significant driver of the workflow design. Ultimately, there are 7 teams that get a vote. For the most part, there is only one representative, but there may be cases when two people are part of the team. So, I used SharePoint permissions groups here to make sure the members could be changed easily.
I couldn't use just one Flexi task action assigned to all of the groups, or just one group with all of the members, because it doesn't allow for everyone to vote. The tasks would close once a majority was reached. With the all must agree options, the tasks closed when there was disagreement. Your options are the following:
When I tried these options, the task would not allow for all responses in the way that we wanted that would be easy to understand for the process owners. So, after trying a few approaches, I went with parallel actions. I wanted to do a streamlined, elegant solution, but I opted for clarity and better supportability for my team later.
Here are a few of the tasks... there are just 7 branches of Flexi tasks. Since all the votes get tallied, there are no actions in any of the specific branches under the Flexi tasks. I'll describe what these are doing in the next section.
Tally the votes
The Flexi tasks have the standard Reject and Approve options, and we also need to know when people don't respond before the time to escalation elapses, so the "No Response" outcome is captured in those instances. The escalation options for the Flexi task look like this:
A key part is to capture both the outcome and the task id for each of the voters. We have 7 total, and below is the config for #3:
After each of the Flexi tasks, I am using a Collection operation to capture all of the task IDs so I can get the specifics later with a list query action.
The Collection operation just adds each task id from the corresponding variable:
Now, we do a For each through the voting tasks in the CollVoteTaskIDs collection. I did a pause for 5 minutes here to make sure all the updates to the items in the task list were complete before trying to loop through them:
Here is the For each configuration
The Query list
The variable "txtCurrentVoteOutcome" is the "Yes", "No", or "No Response" results for each voting group. The "perCurrentAssignedTo" is the person who entered the vote.
Next, I am going to get the display name of the person and start creating a multiline of text field, so later on the process owner can get an email with all of the voters and their responses.
The Set variable looks like this:
And the Build string looks like this:
Now the tally at the end of the For each... I have 3 variables to store the tallied results, numYesVotes, numNoVotes, and numNoResponse. The conditions are looking at the variable I got from the Query list action called txtCurrentVoteOutcome.
In the first condition, if txtCurrentVoteOutcome = "Approve", the Math operation adds one to the numYesVotes variable. In the second condition, if txtCurrentVoteOutcome - "Reject", the Math operation adds one to the numNoVotes variable. If neither of these are true, one is added to the NumNoResponse variable. Then it goes back to the top of the For each and checks the next one.
After the loop, the tallied votes are put into the fields for the item.
At the very end is a final approval checkpoint. This task goes to the process owner before the final "go/no go" meeting.
This is just a regular Flexi task, but it contains all the voting info in the notification to the process owner.
it includes a summary of the number of votes along with the outcome of each vote from the multiline of text variable (mtxtVoteOutcome) that was created earlier.
In total, this voting component is part of a pretty large state machine workflow. The rest of it deals with other parts of the business process, but this voting part was the most complex piece for me to figure out. Hopefully, it gives you some ideas on a way to handle this if you have a similar scenario. Happy Nintexing!