SRP and Workflows…and You!

One of the conveniences about ServiceNow is the ability to easily create new functionality. This is both good and bad. It’s good when citizen developers can create repeatable items that save time and money for the business. It’s bad when those same items need to be extensible or modified, and everything is so tightly coupled, it’s difficult to do so.

The workflow is the perfect example of this. A workflow allows you use a variety of items: tasks, approvals, notifications, events, etc. It’s tempting to create a workflow that fires off notifications, creates approvals, and delegates tasks. But what if one of these components changes? You have to checkout the workflow, check it back in, and hope nothing using the old workflow is in flight and will be affected by the changes negatively.**

The solution? Stricter adherence to the SRP – Single Responsibility Principle.

So a workflow needs to do one thing, and one thing only. Either notifications, approvals, or tasks.

But Justin, a lot of out of the box workflows mix and match functionality!

True.  You also shouldn’t ever modify those workflows. The maintenance of those OOB workflows is not our responsibility provided we leave them alone.  It’s ServiceNow’s responsibility (and this is true of every OOB field/script in the system).  I’ll let them handle those potential issues.  Meanwhile, my business comes to me with needs (maybe employee onboarding or a change process that’s different than the OOB ServiceNow workflow), so I need to manage those workflows on my own.  My opinion is the less a workflow does, the easier it is to manage.  Breaks are isolated, there aren’t dozens of activities to connect, and it’s far more visually appealing.

Also, the OOB Orchestration workflows (that I remember from December 2013, so they may have changed) were excellent at using sub-workflows for various parts of the orchestration process.  “Provision”, for instance, called several sub-flows, each taking care of a specific piece of functionality.  If something went wrong, it was easy to find the phase and debug the problem.

Notifications

Using a manual notifications in a workflow is a major no-no, in my opinion. They are harder to modify and debug than other notifications. They aren’t reusable whatsoever. Avoid using manual notifications in workflows wherever possible.  You may think now “Oh, this will never change”, and then real life happens and it changes twice a month.

Alternative

Notifications are best when coming off an event. There are out of the box business rules for events for practically every major table in ServiceNow. My philosophy is to piggy-back off these rules, using a Script Include for determining the event.  Below is a partial example of how to do this:

Business Rule Example

Name: Incident Events (Custom)
Runs: Before Insert/Update

function onBefore(current, previous) {
var handler = new IncidentEventsHandler(current);
if (current.state.changes()) {
handler.stateChanged();
}
if (current.priority.changes()) {
handler.priorityChanged();
}
}

Script Include
var IncidentEventsHandler = Class.create();
IncidentEventsHandler.prototype = {
STATE_CHANGED: 'incident.state.changed',
initialize: function(record) {
this.record = record;
},
stateChanged: function() {
gs.eventQueue(this.STATE_CHANGED, this.record);
}
};

Similar to this, I have refactored the Approval Events code into a script include, so I can test that it returns the correct event given the circumstances.  (ie: A request moving to “rejected” fires the sc_request.rejected event, etc.)

Following this pattern also lends to the DRY principle – Do Not Repeat Yourself.  Every piece of knowledge needs to have a single, unambiguous source within the system.  If your notifications fire from workflows sometimes, and events in workflows other times, and coded events still other times, how do you debug a defect?  Get everything in once place.  The best place is outside the workflow.

The Catch

Unfortunately, gs.eventQueue doesn’t return anything except undefined, whether it was successful or not. That makes this somewhat hard to unit test, even if we can test that the event name was correct as mentioned earlier. However, we can query the event logs to see if the event was fired and with the correct parameters.

EventTester.js

Now we can test to ensure an event fired and had the correct parameters.  It’s set to just look at incidents from the last minute, which means the assertions will need to be directly after the event would be fired.  You could modify the query condition if you want to search after a series of updates if you wanted to test that way.

We can also test that the notifications had the correct subject test and recipients using this Script Include:

NotificationsTester.js

Each method calls the code from Get/Send Email to speed up the process.  I provided a few convenience methods for things I commonly care about when testing notifications (recipient, subject, related record).  I think body text is another that gets tested, although I think it would be better to use regular expressions to test those once the appropriate records were found.

Approvals

Approvals are the most common thing to use a workflow for in ServiceNow. It’s easy to string together a series of approvals, and the consequences of their outcomes. Hard-coding an approver into the activity is something I’d avoid.  You may sometimes use a field for the approval (ie: assignment_group.manager). However, this can be dangerous as well, if the field is empty. You then need to add an extra check in the workflow for empty fields, or basically a fall-back plan. Once again, I’m going to recommend some scripting for making approvals in the workflow work smoother.

Activity Script
answer = new ApprovalGatherer(current).gatherApprovals();

Script Include
var ApprovalGatherer = Class.create();
ApprovalGatherer.prototype = {
APPROVAL_LIST : 'u_sys_approvers',
initialize: function(record) {
this.record = record;
},
gatherApprovals: function() {
var approvals = [],
approvalRecords = new GlideRecord(this.APPROVAL_LIST);
approvalRecords.addEncodedQuery(this.getEncodedQuery());
approvalRecords.query();
if (approvalRecords.next()) {
approvals = approvalRecords.u_approvers.split(',');
}
return approvals;
},
getEncodedQuery: function() {
if (this.record.task_type === 'sc_req_item') {
return 'approvalForIS' + this.record.cat_item.name;
} else if (this.record.task_type === 'change_request') {
return 'approvalForIS' + this.record.type;
} else {
return 'approvalForISBackups';
}
}
};

In this example, we’re using a custom table called “Sys Approvers” to store a GlideList of users, and a condition to gather those approvers. (For instance, standard change request, a specific catalog item, etc.) This way, changing approvers is as simple as updating data in this table.

In our instance, we have a custom table that store approvers for a certain request type, which was developed by our partners at Cloud Sherpas.  It operates on a similar principle, but instead of using a list to store approvers, the approvers are individual columns in the table.  This is a little easier to work with, but limits the number of approvers you can generate at once.  So the choice is up to you, as to how you’d need this implemented.

Unit Testing

Once again, this is something we can unit test by simply querying the approval table and making sure that at certain phases and for certain conditions, an approval exists and is assigned to the correct user.

Tasks

Tasks are easy enough to do in a workflow or via scripting (and I’ve used both ways effectively). It’s easier to modify and customize tasks that are generated via code and stored as templates in a custom table (or even the sys_template table), but it’s easier to start with tasks in a workflow. Again, we don’t want these in with our approvals. They can be in a workflow that is a sub flow of the approval workflow OR it can be generated on a certain record condition (as in the record is fully approved).

Keeping the DRY principle in mind, I think picking scripting or sub-workflows and sticking to them is the key.  Again, knowing where to go when there’s a problem is what’s most important.  Even if you split table functionality between scripting and sub-workflows (ie: change tasks are automatically generated via code but catalog tasks are generated in workflows) that segments the behavior enough to know where to look for individual applications.  If some catalog tasks are scripted and some are in workflows, it’s going to be a nuisance to find the problem.

Unit Testing

The ways we can unit tests tasks are ensuring the count is correct and ensuring the short description or assignment group are correct. I use code to generate my short descriptions a lot of times because they are often dynamic (ie: the name of the requestor, the type of thing they requested). The name may come from two different fields if it’s a request for a user who doesn’t exist in ServiceNow vs. one who does. So that’s something testable as well. Using the Cart API, you can create requested items with variables and run them through approvals/tasks.

RequestTester.js

Using our unit test framework, we can now create requests and run them through various scenarios (rejection, rework, approval, etc.).  Better yet, we can pair this with NotificationsTester and EventTester to test everything at once.

That’s all for now.  I’d still like to make the browser automation test for Notifications, which will involve similar steps to what I did in NotificationsTester.js, just in a browser with Java code.

** – I am in the middle of working on some code that will detach old instances of workflows to newer instances if you are updating a workflow.  We’ve run into instances where an old workflow is not working properly, and we release a new version of it to production.  In the past, we either prepare for the future error by cancelling the old record and re-creating it, or waiting for it to make an error and manually fixing it once it happens.  Both are manual, time-consuming solutions that can be fixed via script.

Leave a comment