Every check, by category
72 ServiceNow scripting best practices, each with the reason it matters and the fix. Rules last reviewed in October 2026 against the supported releases, Zurich and Australia. Rules tied to a specific release say so; see the ServiceNow release history for how releases and support work.
Correctness 30
All scripts
The script cannot run as written. ServiceNow will throw an error when it is triggered.
Fix: Fix the syntax at the reported line. The rest of the checks still ran on a best-effort reading of the script.
Show example
var gr = new GlideRecord('incident');
gr.addQuery('active', true;
gr.query(); Business rules
In a before rule, changes to current are saved anyway, so update() saves the record twice. In after and async rules it saves the record again and runs every business rule on it again, which can loop.
Fix: Set the fields in a before rule and remove current.update(). If another record must change, update that record instead.
Show example
(function executeRule(current, previous /*null when async*/) {
current.short_description = current.short_description.trim();
current.update();
})(current, previous); Business rules
Inserting or deleting the record the rule is running on creates duplicates or removes the record mid-transaction.
Fix: Abort the action with current.setAbortAction(true), or work on a separate GlideRecord.
Show example
(function executeRule(current, previous /*null when async*/) {
if (current.priority == 1) {
current.insert();
}
})(current, previous); After and async business rules
After and async rules run once the record is already saved, so aborting has no effect.
Fix: Move the validation to a before rule.
Show example
(function executeRule(current, previous /*null when async*/) {
if (current.short_description.nil()) {
current.setAbortAction(true);
}
})(current, previous); Async business rules
Async rules run later, in a separate transaction. The previous object is not available there.
Fix: Use a before or after rule when you need previous values, or pass them in an event.
Show example
(function executeRule(current, previous /*null when async*/) {
if (previous.assigned_to != current.assigned_to) {
gs.eventQueue('incident.reassigned', current);
}
})(current, previous); Business rules
Only display business rules send g_scratchpad to the form. Values set in other rules never reach the browser.
Fix: Move this code to a business rule with When set to display.
Show example
(function executeRule(current, previous /*null when async*/) {
g_scratchpad.isVip = current.caller_id.vip.toString();
})(current, previous); Server scripts
Without conditions this changes or deletes every record in the table.
Fix: Add conditions first and test the same query with a count before running it.
Show example
var logs = new GlideRecord('u_integration_log');
logs.deleteMultiple(); Server scripts
next() returns false until query() has run, so the loop body never executes.
Fix: Call query() after adding conditions and before next().
Show example
var inc = new GlideRecord('incident');
inc.addQuery('priority', 1);
while (inc.next()) {
gs.info(inc.getValue('number'));
} Server scripts
get() returns false when no record matches. Code that carries on anyway works on an empty record.
Fix: Wrap it: if (gr.get(id)) { ... }
Show example
var user = new GlideRecord('sys_user');
user.get('user_name', 'abel.tuter');
gs.info(user.getValue('email')); Server scripts
Without initialize() or newRecord(), field defaults are not applied to the new record.
Fix: Call gr.initialize() (or newRecord()) before setting fields.
Show example
var task = new GlideRecord('task');
task.short_description = 'Follow up';
task.insert(); Server scripts
gr.field is a reference to the field object, not its value. Pushed into an array inside a loop, every entry ends up with the last record’s value.
Fix: Store gr.getValue('field') or gr.field.toString().
Show example
var ids = [];
var inc = new GlideRecord('incident');
inc.addActiveQuery();
inc.query();
while (inc.next()) {
ids.push(inc.sys_id);
} Server scripts
current.state is a GlideElement object, so current.state === 3 is always false.
Fix: Compare current.getValue('state') == '3', or use == which converts the value.
Show example
(function executeRule(current, previous /*null when async*/) {
if (current.state === 6) {
gs.info('Resolved');
}
})(current, previous); Server scripts
Query operators are uppercase ('CONTAINS', 'STARTSWITH', 'IN'). An unknown operator can make the condition match nothing or be ignored.
Fix: Write the operator in uppercase.
Show example
var inc = new GlideRecord('incident');
inc.addQuery('short_description', 'contains', 'vpn');
inc.query(); Client-callable script includes
GlideAjax can only call script includes that extend AbstractAjaxProcessor.
Fix: Use Object.extendsObject(global.AbstractAjaxProcessor, { ... }) as the prototype.
Show example
var UserLookup = Class.create();
UserLookup.prototype = {
getManager: function() {
return '';
},
type: 'UserLookup'
}; Client-callable script includes
Overriding initialize() replaces the parent constructor, so getParameter() and the response no longer work.
Fix: Remove initialize(), or call AbstractAjaxProcessor.prototype.initialize.apply(this, arguments) in it.
Show example
var UserLookup = Class.create();
UserLookup.prototype = Object.extendsObject(global.AbstractAjaxProcessor, {
initialize: function() {
this.table = 'sys_user';
},
getManager: function() {
return '';
},
type: 'UserLookup'
}); GlideAjax calls and script includes
GlideAjax only sends parameters whose names start with sysparm_. Others arrive empty.
Fix: Rename the parameter, for example sysparm_user_id, on both sides.
Show example
var UserLookup = Class.create();
UserLookup.prototype = Object.extendsObject(global.AbstractAjaxProcessor, {
getManager: function() {
var id = this.getParameter('user_id');
return id;
},
type: 'UserLookup'
}); Client-callable script includes
GlideAjax answers are strings. Returning an object sends "[object Object]".
Fix: Return JSON.stringify(result) and JSON.parse() it in the client.
Show example
var UserLookup = Class.create();
UserLookup.prototype = Object.extendsObject(global.AbstractAjaxProcessor, {
getManager: function() {
return { name: 'Abel Tuter', email: '[email protected]' };
},
type: 'UserLookup'
}); Scripted REST, transforms, fix scripts, scheduled jobs
This kind of script does not get a current record, so current is undefined.
Fix: Query the record you need with GlideRecord, or use the objects this script receives (such as target in transforms).
Show example
(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {
return { number: current.getValue('number') };
})(request, response); Transform map scripts
The transform engine saves the target record itself. Saving it in the script writes it twice or creates duplicates.
Fix: Set target fields only; to skip a row use ignore = true.
Show example
(function runTransformScript(source, map, log, target /*undefined onStart*/) {
target.u_imported = true;
target.update();
})(source, map, log, target); Server scripts
new Date() uses the server time zone, not the user’s, and does not understand ServiceNow date formats.
Fix: Use GlideDateTime.
Show example
var today = new Date();
gs.info('Run on ' + today.toDateString()); Server scripts in ES5 mode
Scripts that run in ES5 mode fail on let, const, arrow functions, template literals and other newer syntax.
Fix: Rewrite with var and function, or turn on ECMAScript 2021 mode for the script or the application.
Release: ECMAScript 2021 mode exists from Tokyo (2022). New scoped apps default to it; global scripts default to ES5 and can be switched one by one.
Show example
const groups = ['Service Desk', 'Network'];
groups.forEach((g) => gs.info(`Group: ${g}`)); Client scripts, UI policies, client UI actions
Without sysparm_name the script include does not know which method to run.
Fix: Add ga.addParam('sysparm_name', 'yourMethod');
Show example
function onChange(control, oldValue, newValue, isLoading) {
if (isLoading || newValue === '') { return; }
var ga = new GlideAjax('UserLookup');
ga.addParam('sysparm_user', newValue);
ga.getXMLAnswer(function(answer) {
g_form.setValue('u_manager', answer);
});
} onSubmit client scripts
onSubmit returns before the callback runs, so the form is submitted (or blocked) before the answer arrives.
Fix: Validate with data you already have, or return false, wait for the answer, then resubmit with g_form.submit().
Show example
function onSubmit() {
var ga = new GlideAjax('BudgetUtils');
ga.addParam('sysparm_name', 'isOverBudget');
ga.getXMLAnswer(function(answer) {
if (answer == 'true') return false;
});
} onChange client scripts
onChange scripts also run when the form loads, so the logic fires before anyone changed anything.
Fix: Keep the template guard: if (isLoading || newValue === '') { return; }
Show example
function onChange(control, oldValue, newValue, isLoading) {
g_form.setValue('u_previous_category', oldValue);
} onCellEdit client scripts
The list edit waits for callback() to save or cancel. Without it the edit never completes.
Fix: Call callback(true) to save or callback(false) to cancel on every path.
Show example
function onCellEdit(sysIDs, table, oldValues, newValue, callback) {
if (newValue == 7) {
alert('Close from the form instead');
}
} Client scripts, UI policies, client UI actions
A mandatory field that is empty cannot be hidden; the form keeps it visible and blocks saving.
Fix: Call setMandatory(field, false) before hiding it.
Show example
function onLoad() {
g_form.setMandatory('u_business_reason', true);
if (g_form.getValue('impact') == 3) {
g_form.setDisplay('u_business_reason', false);
}
} Client scripts, UI policies, client UI actions
g_form.getValue() always returns a string, so '10' < '9' is true.
Fix: Use g_form.getIntValue() or getDecimalValue(), or parseInt(value, 10).
Show example
function onLoad() {
if (g_form.getValue('u_quantity') > 10) {
g_form.showFieldMsg('u_quantity', 'Large order', 'info');
}
} All scripts
Server APIs (gs, current, GlideRecordSecure) do not exist in the browser, and client APIs (g_form, g_user, document) do not exist on the server.
Fix: Move the code to the right kind of script, or call the server with GlideAjax.
Show example
function onLoad() {
gs.info('Form loaded for ' + g_user.userName);
} All scripts
if (x = 5) assigns instead of comparing, so the condition is always true.
Fix: Use == or ===.
Show example
var inc = new GlideRecord('incident');
if (inc.get(id)) {
if (inc.state = 6) {
gs.info('Resolved');
}
} All scripts
Older engines read strings with a leading 0 as octal.
Fix: Pass the radix: parseInt(value, 10).
Show example
function onLoad() {
var qty = parseInt(g_form.getValue('u_quantity'));
} Performance 14
Display business rules
Display rules run every time the form is opened. Writing records there slows every form load and changes data when people only look.
Fix: Only read data in display rules. Do the write in a before or after rule.
Show example
(function executeRule(current, previous /*null when async*/) {
var log = new GlideRecord('u_view_log');
log.initialize();
log.u_record = current.getUniqueValue();
log.insert();
})(current, previous); Business rules
The rule still loads and runs for every record, only to do nothing most of the time.
Fix: Move the check to the Condition field or the filter conditions, so the rule only runs when needed.
Show example
(function executeRule(current, previous /*null when async*/) {
if (current.state.changesTo(6)) {
gs.eventQueue('incident.resolved', current);
}
})(current, previous); Before and after business rules
execute() waits for the remote system while the user waits for the form to save. Slow or failing endpoints block saving.
Fix: Use an async business rule, an event and script action, or executeAsync().
Show example
(function executeRule(current, previous /*null when async*/) {
var r = new sn_ws.RESTMessageV2('Jira', 'Create issue');
r.setStringParameterNoEscape('summary', current.short_description);
var response = r.execute();
})(current, previous); Server scripts
A query per loop iteration means hundreds of round trips to the database for hundreds of records.
Fix: Query once before the loop (for example with an IN condition), or use dot-walking or GlideAggregate.
Show example
var inc = new GlideRecord('incident');
inc.addActiveQuery();
inc.query();
while (inc.next()) {
var caller = new GlideRecord('sys_user');
if (caller.get(inc.getValue('caller_id'))) {
gs.info(caller.getValue('email'));
}
} Server scripts
getRowCount() loads every matching record just to count them.
Fix: Use GlideAggregate with addAggregate('COUNT'), or setLimit(1) if you only need to know whether a record exists.
Show example
var inc = new GlideRecord('incident');
inc.addActiveQuery();
inc.query();
gs.info('Open incidents: ' + inc.getRowCount()); Server scripts
query() with no conditions reads the whole table.
Fix: Add addQuery() or addEncodedQuery() conditions, and setLimit() if you only need a few records.
Show example
var users = new GlideRecord('sys_user');
users.query();
while (users.next()) {
gs.info(users.getValue('user_name'));
} Server scripts
Checking if a record exists only needs one row, but the query fetches every match.
Fix: Add gr.setLimit(1) before query().
Show example
var dup = new GlideRecord('incident');
dup.addQuery('correlation_id', id);
dup.query();
if (dup.next()) {
gs.info('Already imported');
} Server scripts
The loop sets the same values on every record and saves each one separately: one database write per record.
Fix: Use updateMultiple() or deleteMultiple() on the same query. Test it first: it does not run per-record logic the way the loop does.
Show example
var inc = new GlideRecord('incident');
inc.addQuery('state', 6);
inc.query();
while (inc.next()) {
inc.state = 7;
inc.update();
} Server scripts
Sleeping holds a worker thread for the whole wait and is not available in scoped apps.
Fix: Schedule the follow-up with an event, a scheduled job or a flow wait instead.
Show example
gs.sleep(5000); // wait for the integration to finish Client scripts, UI policies, client UI actions
Client-side GlideRecord makes synchronous server calls that freeze the form, returns every field, and does not work in Service Portal or scoped apps.
Fix: Use GlideAjax with a client-callable script include, or g_scratchpad from a display business rule.
Release: Not supported in Service Portal (since Helsinki, 2016) or the Next Experience workspaces (since San Diego, 2022).
Show example
function onLoad() {
var user = new GlideRecord('sys_user');
user.get(g_user.userID);
g_form.setValue('location', user.location);
} Client scripts, UI policies, client UI actions
Without a callback, getReference() waits for the server and freezes the form. It is not supported in Service Portal.
Fix: Pass a callback: g_form.getReference('caller_id', function (caller) { ... });
Release: The callback-less form is not supported in Service Portal (since Helsinki, 2016).
Show example
function onLoad() {
var caller = g_form.getReference('caller_id');
if (caller.vip == 'true') {
g_form.addInfoMessage('VIP caller');
}
} Client scripts, UI policies, client UI actions
getXMLWait() is a synchronous call that freezes the browser and is not available in Service Portal.
Fix: Use getXMLAnswer() or getXML() with a callback.
Release: Not supported in Service Portal (since Helsinki, 2016).
Show example
function onLoad() {
var ga = new GlideAjax('UserLookup');
ga.addParam('sysparm_name', 'getManager');
ga.getXMLWait();
g_form.setValue('u_manager', ga.getAnswer());
} Client scripts, UI policies, client UI actions
setValue('ref_field', sysId) makes an extra server call to look up the display value.
Fix: Pass the display value too: g_form.setValue('assigned_to', sysId, displayName);
Show example
function onLoad() {
g_form.setValue('assigned_to', g_scratchpad.managerSysId);
} onLoad client scripts
The form loads, then waits for a second round trip to the server.
Fix: Fetch the data in a display business rule and read it from g_scratchpad.
Show example
function onLoad() {
var ga = new GlideAjax('UserLookup');
ga.addParam('sysparm_name', 'getManager');
ga.getXMLAnswer(function(answer) {
g_form.setValue('u_manager', answer);
});
} Security 6
Client-callable script includes, scripted REST
A value from the client that contains ^ or ^NQ can add its own conditions to the query and read records it should not.
Fix: Use addQuery('field', value) for values that come from a request.
Show example
var UserLookup = Class.create();
UserLookup.prototype = Object.extendsObject(global.AbstractAjaxProcessor, {
getManager: function() {
var gr = new GlideRecordSecure('sys_user');
gr.addEncodedQuery('user_name=' + this.getParameter('sysparm_user_name'));
gr.query();
return gr.next() ? gr.getValue('manager') : '';
},
type: 'UserLookup'
}); Client-callable script includes
Anyone, including people who are not logged in, can call this script include.
Fix: Remove isPublic() unless the data is truly public, and check roles inside each method.
Show example
var UserLookup = Class.create();
UserLookup.prototype = Object.extendsObject(global.AbstractAjaxProcessor, {
isPublic: function() {
return true;
},
getManager: function() {
return '';
},
type: 'UserLookup'
}); Client-callable script includes, scripted REST
GlideRecord ignores ACLs, so any logged-in user who can call this code can read the records it returns.
Fix: Use GlideRecordSecure, or check canRead() before returning data.
Show example
var UserLookup = Class.create();
UserLookup.prototype = Object.extendsObject(global.AbstractAjaxProcessor, {
getManager: function() {
var user = new GlideRecord('sys_user');
if (!user.get(this.getParameter('sysparm_user'))) return '';
return user.getValue('manager');
},
type: 'UserLookup'
}); Client scripts, UI policies, client UI actions
g_user.hasRole() is fine for showing or hiding things, but anything in the browser can be bypassed.
Fix: Enforce access with ACLs on the server as well.
Show example
function onLoad() {
if (!g_user.hasRole('itil')) {
g_form.setReadOnly('priority', true);
}
} All scripts
Passwords and tokens in scripts are readable by every admin and travel in update sets.
Fix: Store them in a credential record, connection alias or encrypted system property.
Show example
var r = new sn_ws.RESTMessageV2();
r.setEndpoint('https://api.example.com/tickets');
r.setBasicAuth('integration.user', 'Winter2026!'); All scripts
Running strings as code is slow, hard to debug, and dangerous with any outside input.
Fix: Call the function directly or use a lookup object.
Show example
var handler = 'handle' + type;
eval(handler + '(current)'); Scoped apps 2
Server scripts
gs.log() and gs.print() are not available in scoped apps and write without a log level.
Fix: Use gs.info(), gs.warn(), gs.error() or gs.debug().
Release: Scoped apps exist since Fuji (2015) and have always blocked gs.log() and gs.print().
Show example
gs.log('Starting nightly cleanup'); Scoped server scripts
JSUtil is a global script include and is not available in scoped apps.
Fix: Use gs.nil() and the other scoped GlideSystem methods.
Release: Scoped apps exist since Fuji (2015).
Show example
if (JSUtil.nil(current.assigned_to)) {
gs.info('Unassigned');
} Upgrade safety 8
Server scripts
setWorkflow(false) skips business rules, notifications, workflows and audit history for the update.
Fix: Only use it when skipping all of those is intended, and leave a comment saying why.
Show example
var inc = new GlideRecord('incident');
if (inc.get(id)) {
inc.setWorkflow(false);
inc.setValue('state', 7);
inc.update();
} Server scripts
The update will not set Updated, Updated by or the update count, which hides who changed the record.
Fix: Only use it for data fixes, and leave a comment saying why.
Show example
var inc = new GlideRecord('incident');
if (inc.get(id)) {
inc.autoSysFields(false);
inc.setValue('u_migrated', true);
inc.update();
} Server scripts
Packages calls use internal Java classes. They are blocked in scoped apps and can break on upgrade.
Fix: Use the documented Glide API that does the same thing.
Release: Blocked in scoped apps since they arrived in Fuji (2015).
Show example
var uuid = Packages.java.util.UUID.randomUUID().toString(); Server scripts
addDays(), getMonth() and similar methods are deprecated because they are ambiguous about time zones.
Fix: Use the LocalTime or UTC variant, for example addDaysLocalTime() or addDaysUTC().
Show example
var due = new GlideDateTime();
due.addDays(3);
gs.info(due.getDisplayValue()); Server scripts
The old JSON script include is legacy.
Fix: Use the built-in JSON.stringify() and JSON.parse().
Show example
var body = new JSON().encode({ number: 'INC0010001' }); Client scripts, UI policies, client UI actions
Form HTML changes between releases and does not exist in Service Portal or Workspace, so DOM code breaks silently.
Fix: Use g_form methods. If you really need the DOM, document why and test after every upgrade.
Release: Service Portal (Helsinki, 2016) and the Next Experience UI (San Diego, 2022) render forms differently, which is what breaks DOM code.
Show example
function onLoad() {
document.getElementById('incident.number').style.fontWeight = 'bold';
} All scripts
The record may not exist, or have a different sys_id, on the next instance.
Fix: Store it in a system property and read it with gs.getProperty(), or look the record up by name.
Show example
var inc = new GlideRecord('incident');
inc.addQuery('assignment_group', '8a5055c9c61122780043563ef53438e3');
inc.query(); All scripts
The URL points to one instance, so links break after cloning or moving to production.
Fix: Build URLs from gs.getProperty('glide.servlet.uri').
Show example
var link = 'https://dev12345.service-now.com/incident.do?sys_id=' + id; Maintainability 12
Business rules
Variables and functions declared at the top level leak into a shared namespace and can clash with other rules.
Fix: Keep the code inside (function executeRule(current, previous) { ... })(current, previous);
Show example
var prefix = 'INC-';
current.u_reference = prefix + current.number; Server scripts
Encoded queries are easy to get subtly wrong, and invalid field names are ignored silently.
Fix: Open it in the Encoded Query Translator to read it in plain English.
Show example
var inc = new GlideRecord('incident');
inc.addEncodedQuery('active=true^priorityIN1,2^assigned_toISEMPTY');
inc.query(); Script includes
The type property should match the script include's name. Mismatches confuse logs and debugging.
Fix: Set type to the same name as the class.
Show example
var PriorityUtils = Class.create();
PriorityUtils.prototype = {
initialize: function() {},
type: 'PriorityHelper'
}; Script includes
The script include silently depends on whichever record the caller has in current.
Fix: Pass the record in as a parameter.
Show example
var PriorityUtils = Class.create();
PriorityUtils.prototype = {
initialize: function() {},
isMajor: function() {
return current.priority == 1;
},
type: 'PriorityUtils'
}; All scripts
On the server it is not where ServiceNow logs go; in the browser it is usually leftover debugging.
Fix: Use gs.info() or gs.debug() on the server, and remove debug output from client scripts.
Show example
console.log('Debug: reached the approval step'); onLoad client scripts
The script only sets fields mandatory, read-only or hidden, which a UI policy does without code.
Fix: Use a UI policy, which is easier to read and maintain.
Show example
function onLoad() {
g_form.setMandatory('category', true);
g_form.setReadOnly('number', true);
} Client scripts, UI policies, client UI actions
alert() blocks the page and looks out of place.
Fix: Use g_form.addInfoMessage(), addErrorMessage() or showFieldMsg().
Show example
function onLoad() {
alert('Remember to attach a screenshot');
} All scripts
Errors are swallowed silently, so failures look like success.
Fix: Log the error with gs.error() (server) or handle it.
Show example
try {
var data = JSON.parse(payload);
} catch (e) {} All scripts
with makes it unclear which variable a name refers to and is not allowed in strict code.
Fix: Use the object name explicitly.
Show example
with (current) {
short_description = 'Updated';
} All scripts
Leftover debugging.
Fix: Remove it.
Show example
function onLoad() {
debugger;
g_form.setValue('state', 2);
} All scripts
Long functions are hard to review and test.
Fix: Split it into smaller functions, for example in a script include.
Show example
function buildReport() {
var total = 0;
total += row0;
total += row1;
// ... 100 more lines
return total;
} All scripts
Unfinished work is about to be deployed.
Fix: Finish it or track it in a story before moving the update set.
Show example
// TODO: remove before go-live
gs.info('Debug mode on');