Script include patterns: where your server logic actually belongs
The three shapes of a script include, a worked TaskUtils example, client-callable boundaries, scoped API design, and how to actually test the thing.
The problem
You hit this the third time you paste the same fifteen lines of GlideRecord logic into a business rule. The first copy felt harmless. The second felt efficient. Then a requirement changes, you fix one copy, and three weeks later someone files a defect against the copy you forgot existed. Every instance I have ever audited had this disease somewhere: real logic smeared across business rules, UI actions, and scheduled jobs, with no single place that owns it. The cure has been in the platform since forever. It's called a script include, and the whole trick is deciding that server logic lives there and nowhere else.
The quick version
A reusable utility class, plus the one-line business rule that calls it:
var TaskUtils = Class.create();
TaskUtils.prototype = {
initialize: function(tableName) {
// default to task so any extension (incident, change...) works
this.tableName = tableName || 'task';
},
// true if the record has not been touched in the last N days
isStale: function(taskGr, days) {
// internal date values sort lexicographically, string compare is safe
return taskGr.getValue('sys_updated_on') < this._daysAgo(days).getValue();
},
// count stale resolved records; only close them when doIt is true
closeStaleResolved: function(days, doIt) {
var q = 'state=6^sys_updated_on<' + this._daysAgo(days).getValue();
var ga = new GlideAggregate(this.tableName);
ga.addEncodedQuery(q);
ga.addAggregate('COUNT');
ga.query();
var total = ga.next() ? parseInt(ga.getAggregate('COUNT'), 10) : 0;
gs.info('TaskUtils: ' + total + ' stale resolved records on ' + this.tableName);
if (!doIt || total === 0)
return total;
var gr = new GlideRecord(this.tableName);
gr.addEncodedQuery(q);
gr.query();
while (gr.next()) {
gr.state = 7;
gr.update();
}
return total;
},
_daysAgo: function(days) {
var gdt = new GlideDateTime();
gdt.addDaysUTC(-1 * days);
return gdt;
},
type: 'TaskUtils'
};
And the business rule:
(function executeRule(current, previous) {
if (new TaskUtils('incident').isStale(current, 30))
current.work_notes = 'Flagged stale by TaskUtils';
})(current, previous);
That's the whole pattern. The business rule states intent in one line. The script include does the work.
Step by step
Pick a shape. Script includes come in three shapes, and choosing the wrong one is a mild but permanent annoyance.
Shape one is the classless function library. The script include's name matches a function, and the loader finds it by that name:
function getActiveCountFor(tableName) {
var ga = new GlideAggregate(tableName);
ga.addQuery('active', true);
ga.addAggregate('COUNT');
ga.query();
return ga.next() ? parseInt(ga.getAggregate('COUNT'), 10) : 0;
}
Fine for one-off helpers. It doesn't scale, because related functions have no shared home and no shared state.
Shape two is the one you saw above: Class.create() with a prototype. This is the default. Reach for it whenever you have two or more related methods, or any state worth holding between calls.
Shape three is extending an existing class with Object.extendsObject, either the platform's (like AbstractAjaxProcessor for client-callable work) or your own base class when several utilities share plumbing.
Use initialize for state, and keep it cheap. initialize runs on every new TaskUtils(). It's the right place to accept configuration, like which table to operate on, and the wrong place to run queries. Callers pass state in once and every method gets it from this, which beats threading a tableName parameter through six method signatures.
Mark internals with an underscore. _daysAgo starts with an underscore because it's an implementation detail. Rhino won't stop anyone from calling it, this is convention, not enforcement. But the convention carries real weight: when you refactor, underscore methods are fair game to change or delete, and public methods are a contract. Every team I have worked with that adopted this rule shipped cleaner refactors for it.
Keep client-callable separate from your internal API. Do not check "Client callable" on TaskUtils. Make a second, thin script include that extends AbstractAjaxProcessor and delegates:
var TaskUtilsAjax = Class.create();
TaskUtilsAjax.prototype = Object.extendsObject(AbstractAjaxProcessor, {
getStaleFlag: function() {
var gr = new GlideRecordSecure('task');
if (!gr.get(this.getParameter('sysparm_task_id')))
return 'false';
return new TaskUtils().isStale(gr, 30) ? 'true' : 'false';
},
type: 'TaskUtilsAjax'
});
The reason is blunt: a client-callable script include is an endpoint. Every method on it can be invoked from a browser by anyone who can read the class name out of your client script. Your internal API has methods that close records in bulk. Those two things should never share a class. The Ajax layer exposes only what the UI needs, validates its parameters, and uses GlideRecordSecure because it runs on behalf of a user.
Call it from business rules and flows the same way. Business rules just do new TaskUtils(), as above. In Flow Designer, an inline script or a script step can do exactly the same, though the better pattern is wrapping the call in a custom Action so flow builders get typed inputs instead of a script field. If you have ever seen gs.include('TaskUtils') in old code, that's the legacy loader from before name-based resolution existed. You don't need it, and finding it in a codebase usually means the surrounding code predates classes too.
What's actually happening under the hood
Script includes are lazy-loaded by name. When the Rhino engine hits an identifier it doesn't recognize, the platform looks for a script include with exactly that name (in scoped apps, the API name, like x_acme_hr.TaskUtils) and evaluates the record's script on that application node, then caches it. Two consequences follow. First, nothing runs until first use, so a hundred script includes cost you nothing at boot. Second, the name is load-bearing: rename the record without renaming the class inside it and resolution silently breaks. The type: 'TaskUtils' property at the bottom isn't decoration either, the platform uses it for type identification, and Object.extendsObject chains rely on it. Keep name, class, and type identical, always.
Scoped apps add one field that deserves respect: "Accessible from". Leave it at "This application scope only" by default. The moment you flip a script include to "All application scopes", every other scope on the instance can build on it, and you now maintain a public API whether you meant to publish one or not. Open it deliberately, for the few classes you actually design as cross-scope services.
Gotchas
The "Client callable" checkbox turns every public method of the class into a browser-reachable endpoint. Never set it on a class that holds write logic, and never rely on obscurity: pair the Ajax layer with GlideRecordSecure and parameter validation. And watch the "Public" flag, which drops the authentication requirement entirely. That combination has featured in more than one instance security review I have run.
Two more that bite quietly. Changing "Accessible from" from all-scopes back to this-scope-only breaks every cross-scope caller at runtime, not at save time, so you find out via incident. And because initialize runs on every construction, a query inside it turns an innocent-looking loop of new TaskUtils() calls into a query storm. Construct once, reuse the instance.
Variations worth knowing
Testing with ATF. A "Run Server Side Script" test step is a perfectly good unit test:
(function(outputs, steps, params, stepResult, assertEqual) {
var utils = new TaskUtils('incident');
var gr = new GlideRecord('incident');
gr.orderByDesc('sys_updated_on');
gr.setLimit(1);
gr.query();
if (!gr.next()) {
stepResult.setOutputMessage('No incidents available to test against');
return false;
}
assertEqual({
name: 'freshly updated record is not stale',
shouldbe: false,
value: utils.isStale(gr, 30)
});
})(outputs, steps, params, stepResult, assertEqual);
Testing with a background script harness. On a PDI, a tiny assert helper gets you most of the value with none of the ceremony:
function check(label, actual, expected) {
gs.info((actual === expected ? 'PASS' : 'FAIL') + ': ' + label +
' (got ' + actual + ', wanted ' + expected + ')');
}
var u = new TaskUtils();
check('default table is task', u.tableName, 'task');
check('dry run returns a number', typeof u.closeStaleResolved(365, false), 'number');
Note the dry run: closeStaleResolved(365, false) counts and logs without touching a record, which is exactly why the doIt flag exists.
Static-style helpers are a nice middle ground: attach functions directly to the class, like TaskUtils.daysBetween = function(a, b) {...}, for pure utilities that need no state. Callers skip construction entirely.
If you take one rule away from this recipe, take this one: business rules stay thin, script includes hold the logic. A business rule should read like a sentence, one condition and one method call. Everything with an algorithm in it belongs in a class you can name, version, secure, and test.