- symptom
The renumber that said it worked
A background script renumbered 23 old requests and reported 23 updates, but every record still started withTASK.- cause
getNextObjNumberPadded()on the record took its number from the parent table’s counter, not the Loaner Request one.- fix
- Named the counter explicitly with
new NumberManager('u_loaner_request'), tested on one record withsetLimit(1), then ran the rest. - lesson
- Check the result, not the row count.
Case study
Two loaner builds
One business process, built twice on ServiceNow: first the way a System Administrator configures it, then the way a developer scripts it. Each build follows the blueprint of an exam I’m preparing for.
◂ Back to work- platform
- ServiceNow · Personal Developer Instance
- builds
- Infinity Loaner · Loaner Tracker
- blueprints
- CSA · CAD
- role
- Solo: design, build, test
- year
- 2026
The problem
TEMPORARY DEVICES, TRACKED
Employees sometimes need a temporary device: their laptop is in for repair, they are travelling, or they have just joined and their equipment has not arrived. The request has to be approved, a device has to be found and handed over, and someone has to notice when it is not returned.
I built that process twice. The first build uses the platform’s configuration tools, the way an administrator would. The second is a scoped application where the rules live in scripts, the way a developer would build it.
- BUILD 01 · CSA
Infinity Loaner Program
A custom table, a catalog request form, an approval flow, a fulfilment board, a dashboard and access controls.
Read build 1 ▸ - BUILD 02 · CAD
Loaner Tracker
A scoped app with its own roles, scripted ACLs, client and server validation, GlideAjax and an outbound REST call.
Read build 2 ▸
Infinity Loaner Program
BUILD 01 · ADMIN · CSA BLUEPRINT · GLOBAL SCOPE
Data model
A Loaner Request table (u_loaner_request) that extends Task, with its own LNR numbering. Number is read-only on this table only, through a dictionary override, so Task and its other child tables are untouched.
Request form
A catalog record producer, Request a Loaner Device. A catalog client script limits the device models to the ones allowed for the request type: Standard gets the Infinity HHD, Executive gets the HHD Pro.
Approval
A Workflow Studio flow runs when a request is created: it asks for approval, then moves the request to Work in Progress, or to Closed Incomplete if it is rejected.
Fulfilment
A Visual Task Board with one lane per state; dragging a card updates the record. Notifications go out when a request is created and when it is assigned.
Reporting
An operations dashboard with totals, overdue loans, and requests by priority, device model, state and assignee.
Security
Requesters can read only the requests they opened. Only itil users can change the device model, and a Deny-Unless ACL locks a request once it is closed.
Device data
The device inventory comes in from a CSV through an import set and transform map. It coalesces on serial number, so a re-import updates existing devices instead of duplicating them.
CMDB
A device CI is related to the Email service it depends on, so the dependency view shows what a missing device affects.
Loaner Tracker
BUILD 02 · DEVELOPER · CAD BLUEPRINT · SCOPED APP
Scoped app
Its own scope and a Loaner Request table that extends Task, numbered LTR, with a Device reference to the CMDB, a loan reason and a return date.
Roles
A chain of three roles: admin contains user, which contains requester. I tested every rule with a user who holds only the user role.
Access controls
Requesters can create, read and edit; only admins can delete. A scripted write ACL allows the record’s owner or an app admin, and field ACLs open the description fields to requesters.
Validation
A UI policy makes the return date mandatory for travel loans. A client script rejects past dates as you type, and a before business rule enforces the same rule on the server.
GlideAjax
When a device is picked, the form calls a client-callable Script Include. It counts the device’s active loans with GlideAggregate and the form warns if the device is already out.
Automation
A flow, Prioritize travel loans, runs when a request is created with Travel as the loan reason and raises it to Priority 2 – High, running as the system user. The app also has a CSV import that coalesces on number, and an outbound REST message tested against a demo API.
function onChange(control, oldValue, newValue, isLoading, isTemplate) {
g_form.hideFieldMsg('device', true); // clear the last warning first
if (isLoading || newValue === '') {
return;
}
var ga = new GlideAjax('x_2103288_loaner_0.LoanerAjaxUtils');
ga.addParam('sysparm_name', 'getActiveLoanCount');
ga.addParam('sysparm_device_id', newValue);
ga.addParam('sysparm_current_id', g_form.getUniqueValue());
ga.getXMLAnswer(function(answer) { // runs when the server replies
if (parseInt(answer, 10) > 0) {
g_form.showFieldMsg('device',
'This device already has ' + answer + ' active loan(s).', 'info');
}
});
}
var LoanerAjaxUtils = Class.create();
LoanerAjaxUtils.prototype = Object.extendsObject(global.AbstractAjaxProcessor, {
getActiveLoanCount: function() {
var deviceId = this.getParameter('sysparm_device_id');
var currentId = this.getParameter('sysparm_current_id');
var ga = new GlideAggregate('x_2103288_loaner_0_loaner_request');
ga.addQuery('device', deviceId);
ga.addQuery('active', true);
ga.addQuery('sys_id', '!=', currentId); // an edited loan must not count itself
ga.addAggregate('COUNT');
ga.query();
return ga.next() ? ga.getAggregate('COUNT') : '0';
},
type: 'LoanerAjaxUtils'
});
Three things that broke
SYMPTOM · CAUSE · FIX · LESSON
- symptom
The date check you could walk around
A past return date was blocked on the form, but the same value saved without complaint when edited from the list.- cause
- Client scripts only run on the form. List editing never loads them.
- fix
- A before business rule with the same check, so the server rejects the save whichever way it arrives.
- lesson
- Client checks are feedback; the rule itself belongs on the server.
- symptom
Fields a requester could not edit
Requesters could write to their own requests, yet Description stayed read-only.- cause
- The table extends Task, so it inherits Task’s field ACLs, and
task.descriptionrequiresitil. - fix
- Field ACLs on the app’s own table for description and short description.
- lesson
- Extending a table inherits its security, not just its fields.
What I’d do next
KNOWN GAPS
The closed-request ACL blocks edits but not deletes. A matching delete rule would close that gap.
State is not on the Infinity Loaner form, so a fulfiller has to use the board or the list to see it.
In Loaner Tracker the requester picks the device, and the form only warns when that device is tied to another open request. A real process would track each device’s own status (In stock, On loan, In repair), update it when a loan starts and ends, and let the fulfiller assign only devices that are in stock.
Most of Build 1 sits in the Default update set; only the last change was captured in a named one. Next time, a named update set from the first change.




