ServiceNow Script Best-Practice Checker

Live

Paste a business rule, client script or script include and get line-by-line warnings for common ServiceNow mistakes, with the fix.

Paste your script

Checking starts as soon as you paste. There is no button to press. Runs in your browser. Nothing is uploaded.

Check the settings

Filled in from your script. Change anything that is wrong; results update instantly.

How to check a script

  1. Paste a script, or open a sample. It is checked as you paste; there is no button to press.
  2. Check the settings. The script type is detected from the code. Two things are not in the code: when a business rule runs, and whether the script is in a scoped app. Set those if they differ.
  3. Fix what is flagged. Click a finding to jump to its line. Each one says what is wrong, why it matters and how to fix it.
  4. Use Copy review to paste the results into a story, change request or pull request.

What the severities mean

High
Will break, lose data or open a security hole. Fix before the change moves on.
Medium
Works today, but is slow, fragile or likely to break after an upgrade.
Low
Worth tidying while you are in the script.
Info
Worth a second look. Not necessarily wrong.

What it does not check (yet)

Script types not covered yet

  • Service Portal widget scripts and UI Builder / Workspace client scripts
  • Flow Designer script steps and custom action scripts
  • ACL scripts, notification email scripts, inbound email actions and script actions
  • Record producer scripts, reference qualifiers, UI scripts and UI pages
  • The GlideQuery API

You can still paste these as a fix script (server) or UI action (client): the general checks run, the context-specific ones do not.

Limits of reading code without running it

  • It follows variables inside the script you paste, not into other script includes or into functions called from a loop.
  • It cannot see your instance: field names, table structure, ACLs, or whether a sys_id exists.
  • It does not know how often a rule runs or what else runs on the same table.

Found a script it gets wrong? Use Report a false positive on the finding, or open an issue on GitHub.

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

Syntax error High

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();
Try it
current.update() in a business rule High

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);
Try it
current.insert() or current.deleteRecord() in a business rule High

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);
Try it
setAbortAction() in an after or async rule High

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);
Try it
previous used in an async rule High

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);
Try it
g_scratchpad set outside a display rule High

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);
Try it
updateMultiple() or deleteMultiple() without a condition High

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();
Try it
next() without query() High

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'));
}
Try it
get() result not checked Medium

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'));
Try it
insert() without initialize() Low

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();
Try it
GlideElement stored instead of its value High

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);
}
Try it
=== on a GlideElement High

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);
Try it
Lowercase query operator Medium

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();
Try it
Client-callable script include without AbstractAjaxProcessor High

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'
};
Try it
initialize() overridden in a GlideAjax script include High

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'
});
Try it
GlideAjax parameter without the sysparm_ prefix High

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'
});
Try it
Object returned to GlideAjax Medium

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'
});
Try it
current is not defined here High

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);
Try it
target.update() or target.insert() in a transform script High

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);
Try it
JavaScript Date on the server Low

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());
Try it
Modern JavaScript in ES5 mode Medium

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}`));
Try it
GlideAjax without sysparm_name High

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);
    });
}
Try it
Asynchronous call in onSubmit High

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;
    });
}
Try it
onChange without the isLoading check Medium

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);
}
Try it
onCellEdit never calls callback() High

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');
    }
}
Try it
Hiding a mandatory field Medium

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);
    }
}
Try it
Comparing getValue() as a number Low

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');
    }
}
Try it
API used on the wrong side High

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);
}
Try it
Assignment inside a condition High

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');
    }
}
Try it
parseInt() without a radix Low

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'));
}
Try it

Performance 14

Database write in a display rule Medium

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);
Try it
Whole script wrapped in one if statement Info

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);
Try it
Outbound REST call in a synchronous rule Medium

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);
Try it
Database query inside a loop High

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'));
    }
}
Try it
getRowCount() to count records Medium

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());
Try it
Query without any condition Medium

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'));
}
Try it
Existence check without setLimit(1) Low

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');
}
Try it
Same update applied record by record Low

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();
}
Try it
gs.sleep() Medium

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
Try it
GlideRecord in a client script High

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);
}
Try it
getReference() without a callback High

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');
    }
}
Try it
getXMLWait() High

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());
}
Try it
Reference field set without a display value Low

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);
}
Try it
GlideAjax in onLoad Low

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);
    });
}
Try it

Security 6

Input concatenated into an encoded query High

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'
});
Try it
isPublic() returns true High

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'
});
Try it
Client-callable code reads data without ACL checks Medium

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'
});
Try it
Security check in the browser Info

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);
    }
}
Try it
Hard-coded credentials High

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!');
Try it
eval() or new Function() High

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)');
Try it

Scoped apps 2

gs.log() or gs.print() Low

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');
Try it
JSUtil in a scoped app High

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');
}
Try it

Upgrade safety 8

setWorkflow(false) Medium

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();
}
Try it
autoSysFields(false) Medium

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();
}
Try it
Direct Java call through Packages Medium

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();
Try it
Deprecated GlideDateTime method Low

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());
Try it
new JSON() Low

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' });
Try it
Direct DOM or jQuery access Medium

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';
}
Try it
Hard-coded sys_id Medium

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();
Try it
Hard-coded instance URL Medium

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;
Try it

Maintainability 12

Business rule code outside the function wrapper Medium

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;
Try it
Encoded query to double-check Info

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();
Try it
Script include type does not match its name Low

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'
};
Try it
current used inside a script include Medium

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'
};
Try it
console.log() Low

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');
Try it
Could be a UI policy Info

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);
}
Try it
alert() Low

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');
}
Try it
Empty catch block Medium

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) {}
Try it
with statement Medium

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';
}
Try it
debugger statement Low

All scripts

Leftover debugging.

Fix: Remove it.

Show example
function onLoad() {
    debugger;
    g_form.setValue('state', 2);
}
Try it
Very long function Info

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;
}
Try it
TODO or FIXME comment Info

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');
Try it

Frequently asked questions

What does the ServiceNow Script Checker look for?

72 best-practice rules for business rules, client scripts, script includes, GlideAjax, UI actions, UI policies, scripted REST resources and transform scripts. They cover correctness (such as current.update() in business rules or === on a GlideElement), performance (queries inside loops, getRowCount()), security (client-callable code without ACL checks, hard-coded credentials, query injection), scoped app compatibility and upgrade safety.

Is my script uploaded anywhere?

No. The script is parsed and checked in your browser. There is no server and nothing is stored, so it is safe to paste code from client projects or production instances.

Do I need to click a button or choose the script type?

No button: the script is checked as soon as you paste it, and again on every change. The script type is detected from the standard ServiceNow templates, such as onChange(), executeRule(), Class.create() and AbstractAjaxProcessor, and the page says which one it found. Two things are not in the code, so set them yourself when they differ: when a business rule runs (before, after, async or display) and whether the script is in a scoped app. Scripts opened from the Update Set Viewer get all of this from the record.

Why does the script type matter?

The same line can be right in one place and wrong in another. current.update() is normal in a server-side UI action but a classic mistake in a business rule, and g_form only exists in the browser.

Which ServiceNow releases does it support?

ServiceNow only supports the two newest family releases, currently Zurich and Australia, and the rules were last reviewed against them in October 2026. Most rules cover long-standing platform behaviour, such as how business rules, GlideRecord and GlideAjax work, which has not changed between releases. The few that depend on a release, such as JavaScript modes, say so on their card. See the ServiceNow release history for how releases and support work. If a finding looks wrong for your release, use Report a false positive on that finding.

What is the difference between ES5 and ECMAScript 2021 mode?

Server scripts traditionally run in ES5 mode, where let, const, arrow functions and template literals fail. Newer releases let scoped applications opt in to ECMAScript 2021 in the application settings. Pick the mode your script runs in; client scripts run in the browser, so the setting does not apply to them.

Does it handle every kind of ServiceNow script?

Not yet. It understands business rules, client scripts (including catalog client scripts), script includes and GlideAjax, UI actions, UI policies, scripted REST resources, transform scripts, fix scripts and scheduled jobs, and the many ways the same code is written in them: different loop styles, ES5 and ES2021 syntax, legacy templates. Service Portal widgets, Flow Designer scripts, ACL and email scripts and GlideQuery are not covered yet; the general checks still run on them. Every rule is tested against examples and common variations before release.

Does it replace testing or Instance Scan?

No. It reads the code without running it, so it catches common mistakes but not every bug. Use it before a code review or while reviewing someone else's update set, and keep testing on a sub-production instance. ServiceNow's own Instance Scan checks the configuration inside your instance; this tool works on any script you can copy, without instance access.

Can I use the results in a code review?

Yes. Copy review copies a Markdown checklist with every finding, its line number and the suggested fix, ready to paste into a story, change request or pull request.