Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Tag incident resolution outliers by z score

## What this solves
Average resolution time hides long-tail outliers. This script calculates mean and standard deviation of resolution minutes and tags incidents whose z score exceeds a threshold, helping teams investigate anomalies.

## Where to use
Run as a Background Script or convert into a Scheduled Job for periodic tagging.

## How it works
- Uses `GlideAggregate` to compute count, mean, and approximate variance
- Calculates z score per resolved incident
- Sets a flag field or work note on outliers above a configurable z threshold

## Configure
- `DAYS`: look-back window
- `Z_THRESHOLD`: default 2.5
- `FLAG_FIELD`: field to set, for example a custom boolean `u_outlier`

## References
- GlideAggregate API
https://www.servicenow.com/docs/bundle/zurich-api-reference/page/app-store/dev_portal/API_reference/GlideAggregate/concept/c_GlideAggregateAPI.html
- GlideRecord API
https://www.servicenow.com/docs/bundle/zurich-api-reference/page/app-store/dev_portal/API_reference/GlideRecord/concept/c_GlideRecordAPI.html
- GlideDateTime API
https://www.servicenow.com/docs/bundle/zurich-api-reference/page/app-store/dev_portal/API_reference/GlideDateTime/concept/c_GlideDateTimeAPI.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Background Script: Tag incident resolution outliers by z score
(function() {
var TABLE = 'incident';
var DAYS = 30;
var Z_THRESHOLD = 2.5;
var FLAG_FIELD = 'u_outlier'; // create this boolean field or change action to add work_notes - potential to change to tag as well if one exists

// Build look-back cutoff
var cutoff = new GlideDateTime();
cutoff.addDaysUTC(-DAYS);

// First pass: mean and std dev of resolution minutes
// Compute duration per record as closed_at - opened_at in minutes
var minutes = [];
var gr = new GlideRecord(TABLE);
gr.addQuery('closed_at', '>=', cutoff);
gr.addQuery('state', '>=', 6); // resolved or closed
gr.addNotNullQuery('opened_at');
gr.addNotNullQuery('closed_at');
gr.query();
while (gr.next()) {
var opened = String(gr.getValue('opened_at'));
var closed = String(gr.getValue('closed_at'));
var mins = gs.dateDiff(opened, closed, true) / 60;
minutes.push({ id: gr.getUniqueValue(), mins: mins });
}
if (!minutes.length) {
gs.info('No records in window. Exiting.');
return;
}

var sum = minutes.reduce(function(a, x) { return a + x.mins; }, 0);
var mean = sum / minutes.length;

var variance = minutes.reduce(function(a, x) {
var d = x.mins - mean; return a + d * d;
}, 0) / minutes.length;
var std = Math.sqrt(variance);

// Second pass: tag outliers
var tagged = 0;
minutes.forEach(function(row) {
var z = std > 0 ? (row.mins - mean) / std : 0;
if (z >= Z_THRESHOLD) {
var r = new GlideRecord(TABLE);
if (r.get(row.id)) {
if (r.isValidField(FLAG_FIELD)) {
r[FLAG_FIELD] = true;
r.update();
} else {
r.work_notes = 'Marked outlier by automation. z=' + z.toFixed(2) + ', mean=' + Math.round(mean) + 'm, std=' + Math.round(std) + 'm';
r.update();
}
tagged++;
}
}
});

gs.info('Outlier tagging complete. Window=' + DAYS + 'd, N=' + minutes.length + ', mean=' + Math.round(mean) + 'm, std=' + Math.round(std) + 'm, tagged=' + tagged);
})();
Loading