S
ervice-now provides the ability to automatically move incidents marked as ‘Resolved’ into a ‘Closed’ state after a certain number of days. In my experience I’ve found that this type of resolution/closure workflow is really the best way to configure your incident management setup because it allows end-users the ability to reopen incidents within a certain window (while they’re still marked as ‘Resolved’) but it also ensures that eventually all of the incident tickets move to a ‘Closed’ state where they won’t be reopened so that you can accurately calculate SLAs and reporting metrics.
The key piece to this auto close functionality is the ‘incident autoclose’ business rule on the ‘Incident’ table. It works in conjunction with the property shown here – that sets the number of days after which a resolved incident will be moved to a closed state. The ‘incident autoclose’ script works great but it is based off of a basic date calculation that doesn’t take into account any business hours or holidays. Shown below are some modified versions of the ‘incident autoclose’ script that take into account the default system calendar (in the case of calendar-based autoclose), or your choice of any system schedule set up in your system (in the case of schedule-based autoclose).
Calendar-based Incident Autoclose Script
// and haven't been updated in a chosen number of days. You
// can set this number as a property in System Properties.
// If you want a comment placed in the incident, uncomment the
// 'gr.comments' line below.
var ps = gs.getProperty('glide.ui.autoclose.time');
var pn = parseInt(ps);
if (pn > 0) {
var gr = new GlideRecord('incident');
gr.addQuery('state', 6);
gr.query();
while(gr.next()){
//Close any records that have not been updated in 'pn' number of days
//Date difference calculated based on default system calendar
var dif = gs.calDateDiff(gs.nowDateTime(), gr.sys_updated_on, true);
if(dif >= pn*86400){
gr.state = 7;
//gr.comments = 'Incident automatically closed after ' + pn + ' days in the Resolved state.';
gr.active = false;
gr.update();
}
}
}
Schedule-based Incident Autoclose Script
// and haven't been updated in a chosen number of days. You
// can set this number as a property in System Properties.
// If you want a comment placed in the incident, uncomment the
// 'gr.comments' line below.
var ps = gs.getProperty('glide.ui.autoclose.time');
var pn = parseInt(ps);
if(pn > 0){
//Get a schedule by name to calculate duration
var schedRec = new GlideRecord('cmn_schedule');
schedRec.get('name', '8-5 weekdays');
if (typeof GlideSchedule != 'undefined')
var sched = new GlideSchedule(schedRec.sys_id);
else
var sched = new Packages.com.glide.schedules.Schedule(schedRec.sys_id);
//Get the current date/time in correct format for duration calculation
var actualDateTime = new GlideDateTime();
actualDateTime.setDisplayValue(gs.nowDateTime());
//Query for resolved incident records
var gr = new GlideRecord('incident');
gr.addQuery('state', 6);
gr.query();
while(gr.next()){
//Close any records that have not been updated in 'pn' number of days
//Date difference calculated based on specified schedule
var dif = sched.duration(gr.sys_updated_on.getGlideObject(), actualDateTime).getDayPart();
if(dif >= pn){
gr.state = 7;
gr.active = false;
//gr.comments = 'Incident automatically closed after ' + pn + ' days in the Resolved state.';
gr.update();
}
}
}
Schedule-based Incident Autoclose Script (BY HOURS!)
// and haven't been updated in a chosen number of hours. You
// can set this number as a property in System Properties.
// If you want a comment placed in the incident, uncomment the
// 'gr.comments' line below.
var ps = gs.getProperty('glide.ui.autoclose.time');
var pn = parseInt(ps);
if(pn > 0){
//Get a schedule by name to calculate duration
var schedRec = new GlideRecord('cmn_schedule');
schedRec.get('name', '8-5 weekdays excluding holidays');
if (typeof GlideSchedule != 'undefined')
var sched = new GlideSchedule(schedRec.sys_id);
else
var sched = new Packages.com.glide.schedules.Schedule(schedRec.sys_id);
//Get the current date/time in correct format for duration calculation
var actualDateTime = new GlideDateTime();
actualDateTime.setDisplayValue(gs.nowDateTime());
//Query for resolved incident records
var gr = new GlideRecord('incident');
gr.addQuery('state', 6);
gr.query();
while(gr.next()){
//Close any records that have not been updated in 'pn' number of hours
//Date difference calculated based on specified schedule
var difDay = sched.duration(gr.sys_updated_on.getGlideObject(), actualDateTime).getDayPart()*24;
var difHour = sched.duration(gr.sys_updated_on.getGlideObject(), actualDateTime).getDurationValue().split(':')[0].substr(-2);
var dif = difDay + parseInt(difHour.replace(/^[0]+/g,""));
if(dif >= pn){
gr.state = 7;
gr.active = false;
//gr.comments = 'Incident automatically closed after ' + pn + ' hours in the Resolved state.';
gr.update();
}
}
}
Thank you.
We have installed the HR plugin but are not able to get the HR cases to auto-close after 5 days. We do have a similar HR autoclose script to reference the correct values for ‘Resolved’ on the HR case. Any thoughts how to get this triggered?
There is a scheduled job (‘System Scheduler -> Scheduled Jobs’) called ‘Autoclose Incidents’ that triggers the ‘incident autoclose’ business rule. You’ll need to duplicate this scheduled job and have it point to your HR business rule (in the ‘Job Context’ field of the scheduled job) in order to trigger the business rule script.
Alternatively, you could simply set up a scheduled script execution from the ‘System Definition -> Scheduled Jobs’ module and paste your HR business rule script in directly there.
I’m testing the scheduled script execution on our General cases (similar to INC and HR cases). I cannot test right now on the HR cases as I have none in my development instance that are in the Resolved state. I am not having any luck with getting them to the closed state. Here is the script (state 25 = Resolved and state 26 = Closed):
function autoCloseGEN() {
var ps = gs.getProperty('glide.ui.autoclose.time');
var pn = parseInt(ps);
if (pn > 0) {
var gr = new GlideRecord('hr');
gr.addQuery('state', '25');
gr.addQuery('sys_updated_on', '<', gs.daysAgoStart(pn));
gr.query();
while(gr.next()) {
gr.state = '26';
// gr.comments = 'GENERAL case automatically closed after ' + pn + ' days in the Resolved state.';
gr.active = false;
gr.update();
}
}
}
If you need additional help debugging custom scripts you may need to go to the Service-now forums for assistance. I can tell you that the script here is querying for HR records (which wouldn’t work if you’re trying to close records outside of that table). You are also querying for (and setting) state values and putting them in quotes. The only things that need to be in quotes when you’re using script are string or text values. Those state values are actually integers so you should remove the quotes around ’25’ and ’26’ in your script.
A good troubleshooting tip is to add ‘gs.log’ commands at different places in your script to see where the script might be breaking.
Mark, thank you very much for this!
I’m curious whether the query and update should be for incident_state or for state or if it doesn’t matter (for Incidents, of course).
Thanks again!
Good question. It does matter. If you use incident state for your incident table you need to change the script accordingly. I prefer to use the ‘state’ field from the task table for my clients so the scripts here will usually use ‘state’ rather than ‘incident_state’.
Great clarification, thanks! I’ll consider trying to use the task table more often as well, allowing the scripts to be more portable or global in nature.
Performance Question — won’t this query all resolved incidents every time an incident is touched?
Why not schedule an event to close the specific incident?
Sounds like a nice idea but I don’t see how I can queue and event for the future.
Good question. It will query all resolved incidents but it’s certainly not anywhere close to a performance killer. The problem with queueing an event is that you then have to deal with the process of un-queueing that event (or ignoring it) if the incident moves back to an active status. That could be managed but you’re not really buying yourself much of anything in the way of performance and you’re adding a lot more scripting and process complexity.
I thought this was running when any incident record was accessed, but it isn’t — my bad.
I now see it is run on the hour by the system scheduler. That’s different.
Thanks for nice post regarding schedules. Do you know how to check current time is within a schedule? I tried to do the check with this kind of code, but it seems that the IF statement does not work 🙁
******
gs.addInfoMessage(“test”);
var schedRec = new GlideRecord(‘cmn_schedule’);
schedRec.get(‘name’, ‘8-5 weekdays excluding holidays’);
var sched = new Packages.com.glide.schedules.Schedule(schedRec.sys_id);
gs.addInfoMessage(sched.name);
if (sched.isInSchedule(new GlideDateTime()))
{
gs.addInfoMessage(“test2″);
current.schedule = ”;
}
else
{
gs.addInfoMessage(“test3”);
}
******
I don’t see any obvious issues with your code. You might try the forums on this one though as it’s not directly related to the solution in this post. Here’s a place you could start.
http://community.service-now.com/forum/7180
Thanks for this! I made a small modification to the script to look for an auto close records of a different table. The “pending confirmation” State value is “-5” (long story but not going to change it unless I really need to). Using this code, a bunch of tickets that were last updated on “2012-05-30” is calculated as 4 days old. Not sure why. Would really appreciate it if you can ID something wrong with the code:
// This script automatically closes Tickets that are resolved
// and haven’t been updated in a chosen number of days.
//var ps = gs.getProperty(‘glide.ui.autoclose.time’);
gs.log(“Starting auto-close ticket job”);
var ps = 5;
var pn = parseInt(ps);
if(pn > 0){
//Get a schedule by name to calculate duration
var schedRec = new GlideRecord(‘cmn_schedule’);
schedRec.get(‘name’, ‘8-5 weekdays’);
var sched = new Packages.com.glide.schedules.Schedule(schedRec.sys_id);
//Get the current date/time in correct format for duration calculation
var actualDateTime = new Packages.com.glide.glideobject.GlideDateTime();
actualDateTime.setDisplayValue(gs.nowDateTime());
//Query for resolved ticket records
var gr = new GlideRecord(‘ticket’);
gr.addQuery(‘state’, -5);
gr.query();
while(gr.next()){
//Close any records that have not been updated in ‘pn’ number of days
//Date difference calculated based on specified schedule
var dif = sched.duration(gr.sys_updated_on.getGlideObject(), actualDateTime).getDayPart();
gs.log(gr.number + ” is ” + dif + ” days old.”);
if(dif >= pn){
//gr.state = 3;
//gr.active = false;
//gr.comments = ‘Incident automatically closed after ‘ + pn + ‘ days in the Resolved state.’;
//gr.update();
}
}
}
I think that you’re probably getting the correct result. Remember that a day is a 24 hour period. If your schedule is an 8 hour time period each day, then that would put you about 15 calendar days back. I think 2 instead of 5 will get you a closer result.
Mark,
Thanks for posting this code. Just curious if there is a reason you’re not using sched.duration().getNumericValue() to get the schedule duration in ms? By my testing these two blocks get the same result:
Original:
var difDay = sched.duration(gr.sys_updated_on.getGlideObject(), actualDateTime).getDayPart()*24;
var difHour = sched.duration(gr.sys_updated_on.getGlideObject(), actualDateTime).getDurationValue().split(‘:’)[0].substr(-2);
var dif = difDay + parseInt(difHour.replace(/^[0]+/g,””));
getNumericValue:
var dif = Math.floor((sched.duration(gr.sys_updated_on.getGlideObject(), actualDateTime).getNumericValue())/(60*60*1000)));
Thanks again for sharing your code.
-Brian
No particular reason other than I found something that worked and went with it. Your solution may work just as well. If you use it with your modification, let me know how it’s working after it’s been in production for a few months. I hesitate to change a working solution without a lot of testing behind the replacement.
Hi,
I was wondering, if there is/was a specific reason to use the Package call instead of the DurationCalculator. (Beside the fact that a working solution was found which works well)
Looking at the wiki article (http://wiki.servicenow.com/index.php?title=DurationCalculator) also here it is possible to define a schedule for the calculation.
Andreas
No reason other than the Packages call provided a working solution. I’ve updated all of the code above to work for both Packages calls and non-packages calls so it shouldn’t be an issue when Calgary comes along.
Thx – yes, I saw that one 🙂 Looking at the code it works great. I am using your approach to prepare our own code for the upgrade to Calgary.
I was thinking of tackling this similar but I am missing one piece to get it working.
The idea is to perform the date and time calculation in the beginning (also based on a schedule) and then adding it to the query to find only those records which have been updated last on the calculated date and time or earlier.
This would make the code less expensive for the server as the difference no longer needs to be calculated for each found record.
Depending on the amount of found records it could make a difference.
All functions and methods I found so far were about to “add” time but not to “subtract” it. Any idea if there is something out there?
Hi,
Do you have any idea what may be the reason that incident autoclose scheduled job causes performance problems? I used exactly the same code as in this article. Code is run as scripted scheduled job, once per hour. Did a test and closing 10 incidents takes approximately 14 – 18 seconds. That’s very bad because we have 40 000 incidents to be closed every day. After switching of the workflow (gr.setWorkflow(false)) before calling gr.update() – it works very fast. Is there any good way how to check what business rules, workflows and other kind of events are run on “incident_state” and “active” fields update? I found 15 custom or in-built business rules that are triggered but none of these is performance killer. Thanks in advance for any tip.
– Adam
Sounds like the autoclose job isn’t the source of the performance issues if setting the ‘setWorkflow’ flag to false removes the performance issue. The only thing I can say is to review the business rules on the incident and task tables. You can use business rule debugging and manually change an incident from resolved to closed to see which business rules are being run.
Hi.
The above script works fine except for the fact that it displays the time in GMT. I know that we can change that by adding a setDisplay() method but that converts the variables into string format and they can’t be used in Schedule.Duration function. Any idea how can I do it in system’s time zone?
I don’t think the scripts above display the time anywhere, do they? If you’re looking for help with the notifications or business rules that set field values associated with autoclose you should post that on the ServiceNow community. If I’m missing something please let me know and I’ll attempt to address that.
Hi Mark,
Can we use this code in Istanbul version?
Hi JM,
This code hasn’t been tested in Istanbul specifically. If you want to try it we recommend applying it to a sub-production instance and thoroughly testing it before moving it into production.
No they don’t.
But if we try to print out the variables “actualDateTime.setDisplayValue(gs.nowDateTime())” and gr.sys_updated_on.getGlideObject() they give the time in GMT format.
What if I wanted the time in system’s/user’s time zone?