Home / Code Style Guide
ServiceNow JavaScript Style Guide
Version 1.1 — Global Scope | Rhino ES5 Engine
Applies to all server-side scripting on the ServiceNow platform: Script Includes, Business Rules, Scheduled Jobs, Fix Scripts, Transform Map scripts, and Script Actions. Tuned for the Rhino ES5 engine and Global-scope development.
1. Engine Constraints
ServiceNow server-side scripts execute in Mozilla Rhino, an ES5 engine. This is not negotiable and drives most of the rules below.
Not available (server-side, Global scope):
| Feature | Status | Use instead |
|---|---|---|
let / const | ✗ | var |
| Arrow functions | ✗ | function () {} |
| Template literals | ✗ | String concatenation |
| Destructuring | ✗ | Explicit property access |
| Default parameters | ✗ | arg = arg || default; |
| Spread / rest | ✗ | Array.prototype.slice.call(arguments) |
class | ✗ | Class.create() / prototype |
Promise / async | ✗ | Synchronous code |
Array.prototype.includes | ✗ | indexOf(x) > -1 |
Object.assign | ✗ | Manual copy loop |
JSON.parse / stringify | ✓ | Available |
Array.prototype.forEach/map/filter | ✓ | Available (ES5) |
Object.keys | ✓ | Available (ES5) |
Enable ES2021 mode by setting glide.script.ecma.version to ES2021 — but do NOT rely on it in Global scope unless the whole instance is validated. Legacy Global code and third-party plugins commonly break. If your instance has it enabled, document that fact at the top of this guide and relax Section 3 accordingly.
Rule: Write ES5 unless you have written confirmation that ES2021 mode is on and stable.
2. Naming Conventions
| Artifact | Convention | Example |
|---|---|---|
| Script Include | PascalCase, suffix by role | GroupAuditEngine, IncidentUtils |
| Function / method | camelCase, verb-first | getActiveMembers(), buildQuery() |
| Local variable | camelCase | var groupSysId |
| GlideRecord variable | gr prefix + entity | grIncident, grGroupMember |
| GlideAggregate variable | ga prefix | gaIncidentCount |
| Boolean | is / has / should prefix | isActive, hasManager |
| Constant | SCREAMING_SNAKE_CASE | var MAX_BATCH_SIZE = 500; |
| Private method | Leading underscore | _buildEncodedQuery() |
| Custom table | u_ prefix, snake_case | u_group_audit_finding |
| Custom field | u_ prefix, snake_case | u_finding_severity |
| System property | Dotted namespace | x.group_audit.batch_size |
Rationale for gr prefixing: Rhino has no type system. gr prefixes make it visually obvious at the call site that a variable carries GlideRecord semantics — meaning .next(), .query(), and dot-walking are in play, and that assigning it to another variable does not copy it.
Never name a variable current or previous in a Script Include. Those are Business Rule globals and shadowing them causes silent breakage when the Script Include is called from a BR context.
3. Variable Declarations
One var per line. No comma chaining.
// Good
var grIncident = new GlideRecord('incident');
var count = 0;
var isValid = false;
// Bad — comma chaining hides hoisting and breaks diffs
var grIncident = new GlideRecord('incident'),
count = 0,
isValid = false;
Declare at the top of the function scope. Rhino hoists var to the top of the enclosing function, not the enclosing block. Declaring inside an if or for block is a lie about the variable's lifetime.
// Good
function processGroups(groupIds) {
var i;
var grGroup;
var results = [];
for (i = 0; i < groupIds.length; i++) {
grGroup = new GlideRecord('sys_user_group');
// ...
}
return results;
}
// Bad — `grGroup` is function-scoped anyway, the `var` inside the loop is misleading
for (var i = 0; i < groupIds.length; i++) {
var grGroup = new GlideRecord('sys_user_group');
}
Never use implicit globals. An undeclared assignment leaks into the global object and, in ServiceNow, can pollute across script execution in the same transaction.
// Catastrophic
function setup() {
counter = 0; // implicit global
}
Default parameters:
function query(table, limit) {
limit = limit || 100; // careful: 0 is falsy
// If 0 is a valid value:
limit = (typeof limit === 'undefined') ? 100 : limit;
}
4. Script Include Structure
Use Class.create() for instantiable utilities. Use a plain object literal for pure static helpers.
Standard instantiable form:
var GroupAuditEngine = Class.create();
GroupAuditEngine.prototype = {
/**
* @param {Object} [options]
* @param {number} [options.batchSize=500]
*/
initialize: function (options) {
options = options || {};
this.batchSize = options.batchSize || GroupAuditEngine.DEFAULT_BATCH_SIZE;
this.findings = [];
},
/**
* Runs the full audit and persists findings.
* @returns {number} Count of findings written.
*/
run: function () {
this._collectFindings();
return this._persistFindings();
},
/** @private */
_collectFindings: function () {
// ...
},
/** @private */
_persistFindings: function () {
// ...
},
type: 'GroupAuditEngine'
};
// Constants hang off the constructor, not the prototype.
GroupAuditEngine.DEFAULT_BATCH_SIZE = 500;
The type property is mandatory. ServiceNow uses it for reflection and it appears in stack traces. Omitting it produces [object Object] in logs.
Client-callable Script Includes must extend AbstractAjaxProcessor:
var PriceCalculatorAjax = Class.create();
PriceCalculatorAjax.prototype = Object.extendsObject(AbstractAjaxProcessor, {
getFinalPrice: function () {
var basePrice = parseFloat(this.getParameter('sysparm_base_price'));
if (isNaN(basePrice)) {
return '0';
}
return String(new PriceCalculatorUtil().applyDiscount(basePrice));
},
type: 'PriceCalculatorAjax'
});
Rule: Never put business logic in the AJAX processor. It is a thin, sanitizing façade over a real, testable Script Include. The AJAX class validates and coerces parameters; the util class computes.
Static-only helper:
var DateUtils = {
toGlideDateTime: function (isoString) {
// ...
}
};
5. GlideRecord Patterns
5.1 Always call setLimit() when you expect one record
// Good
var grUser = new GlideRecord('sys_user');
grUser.addQuery('user_name', userName);
grUser.setLimit(1);
grUser.query();
if (grUser.next()) {
// ...
}
Without setLimit(1), the platform prepares to stream the full result set. On sys_user with a non-indexed predicate this is the difference between a 4ms and a 4000ms transaction.
5.2 Prefer get() for sys_id lookups
var grGroup = new GlideRecord('sys_user_group');
if (grGroup.get(groupSysId)) {
// ...
}
get() on a sys_id short-circuits query planning entirely.
5.3 setColumns() — and the trap
setColumns() restricts the SELECT list. It is the single highest-leverage optimization on wide tables (task, cmdb_ci, sys_user).
var grIncident = new GlideRecord('incident');
grIncident.addActiveQuery();
grIncident.setColumns('number,assignment_group,short_description');
grIncident.query();
while (grIncident.next()) {
log(grIncident.getValue('number'));
}
⚠ The trap: accessing a field not in setColumns() returns an empty string, silently. No exception. No warning. Your logic quietly takes the wrong branch.
grIncident.setColumns('number');
grIncident.query();
grIncident.next();
grIncident.getValue('short_description'); // '' — not an error
Rule: every setColumns() call and every subsequent getValue() in that loop must be reviewed together as one unit. If you add a field read, you add it to setColumns() in the same commit. sys_id is always returned implicitly — you do not need to list it.
5.4 Never construct a GlideRecord inside a loop over another GlideRecord
This is the N+1 pattern and it is the most common cause of instance-wide semaphore exhaustion.
// Catastrophic — one query per incident
while (grIncident.next()) {
var grUser = new GlideRecord('sys_user');
grUser.get(grIncident.getValue('assigned_to'));
names.push(grUser.getValue('name'));
}
// Good — dot-walk, the platform joins
grIncident.setColumns('number,assigned_to.name');
while (grIncident.next()) {
names.push(grIncident.assigned_to.name.toString());
}
// Good — or batch with a single IN query
var userIds = [];
while (grIncident.next()) {
userIds.push(grIncident.getValue('assigned_to'));
}
var grUser = new GlideRecord('sys_user');
grUser.addQuery('sys_id', 'IN', userIds.join(','));
grUser.query();
5.5 getValue() over dot-notation for reads
var state = grIncident.getValue('state'); // string, or null
var state = grIncident.state; // GlideElement object
var state = grIncident.state.toString(); // string, verbose
getValue() returns a primitive String or null. Bare dot-notation returns a GlideElement, which coerces to a string in most contexts but not in === comparisons, JSON.stringify(), or Array.prototype.indexOf(). Use getValue() for reads and setValue() for writes; reserve dot-notation for dot-walking to a related record's field.
// Silent bug
if (grIncident.state === '2') { } // always false — GlideElement !== String
// Correct
if (grIncident.getValue('state') === '2') { }
5.6 Use GlideAggregate for counts
// Bad — streams every row to count them
var grIncident = new GlideRecord('incident');
grIncident.addActiveQuery();
grIncident.query();
var count = grIncident.getRowCount();
// Good — COUNT(*) at the database
var gaIncident = new GlideAggregate('incident');
gaIncident.addActiveQuery();
gaIncident.addAggregate('COUNT');
gaIncident.query();
var count = gaIncident.next() ? parseInt(gaIncident.getAggregate('COUNT'), 10) : 0;
5.7 Encoded queries
Build encoded queries from addQuery() calls, not by string concatenation. If you must accept an encoded query as input, never interpolate user data into it.
// Good
grIncident.addQuery('state', 'IN', ['1', '2'].join(','));
grIncident.addQuery('opened_at', '>=', gs.beginningOfLastMonth());
// Injection risk
grIncident.addEncodedQuery('short_descriptionLIKE' + userInput);
6. Business Rule Rules
Wrap every Business Rule in an IIFE. ServiceNow provides the boilerplate; do not delete it. Without it, your var declarations leak into the shared execution scope of every other Business Rule in the same transaction.
(function executeRule(current, previous /*null when async*/) {
// ...
})(current, previous);
Choose the right timing:
| Need | Timing |
|---|---|
| Modify fields on the record being saved | before |
| Abort the operation | before + current.setAbortAction(true) |
| Act on other records | after |
| Non-urgent side effects (notifications, integrations) | async |
| Enforce row-level read security | before query |
| Modify what the user sees without a DB write | display |
Never call current.update() in a before Business Rule. The platform is already going to write the record. Calling update() re-enters the rule engine and causes recursion. Just assign the field.
// Catastrophic
(function executeRule(current, previous) {
current.setValue('priority', '1');
current.update(); // NO
})(current, previous);
// Correct
(function executeRule(current, previous) {
current.setValue('priority', '1');
})(current, previous);
previous is null in async rules. Guard every access.
Always scope the rule with a Condition or Filter Conditions, not with an early return inside the script. Conditions are evaluated by the rule engine before the script is compiled and executed; an early return still pays the compilation cost on every insert/update.
Business Rules must be thin. More than ~20 lines means the logic belongs in a Script Include the rule calls.
7. Error Handling
Never swallow an exception.
// Unacceptable
try {
riskyOperation();
} catch (e) {}
// Unacceptable — hides the stack
try {
riskyOperation();
} catch (e) {
gs.info('something went wrong');
}
// Correct
try {
riskyOperation();
} catch (e) {
gs.error('[GroupAuditEngine] run() failed for group ' + groupSysId +
': ' + e.message + '\n' + e.stack);
throw e; // or handle deliberately and document why
}
Rhino exceptions: e.message and e.stack are available. Java exceptions surfacing through Glide APIs may not populate e.stack — log e directly as a fallback.
Do not use exceptions for control flow. Rhino exception construction is expensive (full stack capture).
Validate at the boundary. Script Include public methods validate their inputs and throw. Private methods (_prefixed) may assume inputs are already valid.
run: function (groupSysId) {
if (!groupSysId || typeof groupSysId !== 'string') {
throw new Error('GroupAuditEngine.run: groupSysId is required');
}
return this._runInternal(groupSysId);
}
8. Logging
Use gs.info / gs.warn / gs.error. Never gs.log() — it writes to syslog with no source and is deprecated for new code. Never gs.print() outside a Fix Script.
Every log line is prefixed with the artifact name in brackets.
gs.info('[GroupAuditEngine] Processed ' + count + ' groups in ' + elapsed + 'ms');
This makes syslog filterable: message LIKE [GroupAuditEngine].
Log levels:
| Level | Use for |
|---|---|
gs.error | Something failed and a human must look |
gs.warn | Degraded but recovered; unexpected data |
gs.info | Lifecycle milestones — job start/end, counts |
gs.debug | Per-record detail; off in production |
Never log inside a while (gr.next()) loop in production code. A 50,000-row loop writes 50,000 syslog rows synchronously and will stall the transaction. Accumulate and log once.
// Bad
while (grIncident.next()) {
gs.info('[Audit] checking ' + grIncident.getValue('number'));
}
// Good
var processed = 0;
while (grIncident.next()) {
processed++;
}
gs.info('[Audit] checked ' + processed + ' incidents');
Guard debug logging behind a system property.
var DEBUG = gs.getProperty('x.group_audit.debug', 'false') === 'true';
if (DEBUG) {
gs.debug('[GroupAuditEngine] ' + JSON.stringify(finding));
}
Never log PII, credentials, or full record payloads. See Section 10.
9. Comments and Documentation
Use JSDoc on every public Script Include method. ServiceNow's Studio and VS Code both consume it.
/**
* Returns active members of a group, excluding locked-out users.
*
* @param {string} groupSysId - sys_id of sys_user_group.
* @param {boolean} [includeInactive=false] - Include inactive users.
* @returns {string[]} Array of sys_user sys_ids. Empty array if none.
* @throws {Error} If groupSysId is falsy.
*/
getActiveMembers: function (groupSysId, includeInactive) {
// ...
}
Comment the why, never the what.
// Bad
i++; // increment i
// Good
// Ordered by sys_created_on DESC because the audit table is append-only
// and we only care about the latest finding per group.
grFinding.orderByDesc('sys_created_on');
Every non-obvious query gets a comment explaining the index it relies on.
// Relies on the composite index (assignment_group, state). Reordering these
// addQuery calls will not change the plan, but dropping assignment_group will.
grIncident.addQuery('assignment_group', groupSysId);
grIncident.addQuery('state', '!=', '7');
Header block on every Script Include:
/**
* GroupAuditEngine
*
* Scans sys_user_group for governance violations (missing manager,
* inactive manager, empty group) and writes findings to
* u_group_audit_finding.
*
* Scope: Global
* Owner: Platform COE
* Called by: GroupAuditScheduledJob, GroupAuditFixScript
*/
10. Security
No eval(). No Function() constructor. No GlideEvaluator on user input. Ever. There is no legitimate use case in application code.
No gs.executeNow() / GlideScriptedProcessor on user-controlled strings.
Always use GlideRecordSecure when the query is on behalf of a user.
// Honors ACLs
var grIncident = new GlideRecordSecure('incident');
GlideRecord bypasses read ACLs. In a Scripted REST API, a Processor, or any code reachable from an unprivileged session, GlideRecord is a data-leak vector.
In Scripted REST APIs, never trust request.pathParams or request.queryParams.
var sysId = request.pathParams.sys_id;
if (!/^[0-9a-f]{32}$/.test(sysId)) {
response.setStatus(400);
return;
}
Never interpolate into addEncodedQuery(). See 5.7.
gs.hasRole() is not an authorization check in a Script Include called from an elevated context. It reflects the current session's roles. If a Scheduled Job runs as system, gs.hasRole('admin') returns true regardless of who queued the work. Pass the acting user explicitly.
Never log secrets. Credentials, tokens, sys_user.password hashes, MID Server keys, and OAuth client secrets must never reach syslog. syslog is readable by itil in many instances.
System properties holding secrets must be private and their Read roles set. Better: use a Credential record and sn_cc.StandardCredentialsProvider.
Table-level protection: for row-level read security, use a before query Business Rule that mutates current.addQuery(...). Do not attempt to filter in the script after the query returns — the rows have already been read.
11. Performance Patterns
11.1 The N+1 rule
Restated because it matters most: the number of GlideRecord objects constructed must not scale with the number of rows processed. If you can trace a new GlideRecord() to inside a while (x.next()), it is a defect.
11.2 Batch writes
GlideRecord.insert() in a loop issues one round trip per row. For > 1000 rows, use GlideMultipleUpdate (updates) or accept the cost and chunk with progress logging.
var gmu = new GlideMultipleUpdate('incident');
gmu.addQuery('assignment_group', oldGroup);
gmu.setValue('assignment_group', newGroup);
gmu.execute();
GlideMultipleUpdate bypasses Business Rules and audit. That is usually the point — but it must be a deliberate, documented decision.
11.3 Cache system property reads
gs.getProperty() hits the property cache, which is cheap, but not free. Read once into a local at the top of the function.
initialize: function () {
this.batchSize = parseInt(gs.getProperty('x.group_audit.batch_size', '500'), 10);
}
11.4 Prefer setWorkflow(false) and autoSysFields(false) for bulk data ops
grStaging.setWorkflow(false); // suppress Business Rules
grStaging.autoSysFields(false); // preserve sys_created_on / sys_updated_by
grStaging.update();
Document every use. Suppressing Business Rules on a table with governance rules is how audit findings get created.
11.5 Never getRowCount() on an unlimited query
It forces the platform to materialize the full result set. See 5.6.
11.6 orderBy requires an index
An orderBy on an unindexed column on a large table causes a filesort. On task with 8M rows this is a multi-minute operation that holds a semaphore the entire time.
11.7 Chunk long-running jobs
Any Scheduled Job that can exceed 30 seconds must chunk and checkpoint. Store the last-processed sys_id in a system property and resume. A job that runs for 20 minutes is holding a scheduler worker thread, and there are only 4–8 of them per node.
12. Hard Prohibitions
These are not style preferences. Each has caused a production incident somewhere.
eval(),new Function(),GlideEvaluatoron any non-literal input.current.update()inside abeforeBusiness Rule.new GlideRecord()inside awhile (other.next())loop.gr.getRowCount()withoutsetLimit().- Catching an exception and not logging it.
gs.log()in new code.- Logging inside a row-iteration loop.
GlideRecord(notGlideRecordSecure) in a Scripted REST API on user-visible data.- Interpolating user input into
addEncodedQuery(). - Reading a field not present in
setColumns(). - Naming a Script Include variable
currentorprevious. - Omitting the
type:property from aClass.create()prototype. setWorkflow(false)without a comment explaining why.- Implicit globals (assignment without
var). - A Business Rule longer than ~20 lines.
13. Formatting & Syntax Conventions
13.1 Keyword vs. function-call spacing
Keywords take a space before the parenthesis. Function calls do not.
// Good
if (isValid) { }
while (grIncident.next()) { }
for (var i = 0; i < n; i++) { }
switch (state) { }
catch (e) { }
function (arg) { } // anonymous
function getName(arg) { } // named — no space after the name
// Bad
if(isValid) { }
while(grIncident.next()) { }
getName (arg);
Reason: the space is a visual signal that the parentheses are part of a control structure, not an invocation. if (x) is a condition; f(x) is a call. When scanning a 400-line Business Rule, this distinction lets you find the control flow without parsing. It is also the ESLint default (keyword-spacing, space-before-function-paren: { named: 'never', anonymous: 'always' }) and the dominant JavaScript convention — deviating means fighting every tool and every reader.
Note that function getName(arg) takes no space, because the name is what's being defined and getName(arg) mirrors how it will be called. The anonymous form function (arg) takes a space because there is no name to bind to.
13.2 Braces
K&R / One True Brace. Opening brace on the same line. Always braces, even for one-liners.
// Good
if (isValid) {
process();
}
// Bad — Allman wastes a line and is not the JS convention
if (isValid)
{
process();
}
// Catastrophic — the classic goto fail bug
if (isValid)
process();
cleanup(); // always runs
Reason for mandatory braces: the single-statement form is a latent bug. Adding a second line to the body without adding braces changes semantics silently, and the indentation lies about it. This is not hypothetical — Apple's goto fail TLS vulnerability was exactly this.
13.3 Indentation
Four spaces. No tabs. ServiceNow's script editor renders tabs inconsistently and the platform stores script fields as raw text; a mixed-indent script is unreadable in the Studio diff view and in sys_update_xml payloads.
13.4 Semicolons
Always. Never rely on Automatic Semicolon Insertion.
// Good
var x = 1;
return x;
// Catastrophic — ASI inserts a semicolon after `return`
return
{
ok: true
}; // returns undefined
13.5 Equality
=== and !== always. Never == / !=.
if (grIncident.getValue('state') === '2') { }
Reason: in ServiceNow, == interacts badly with GlideElement. grIncident.state == '2' invokes valueOf() and appears to work; grIncident.state === '2' correctly returns false because a GlideElement is not a String. Using == masks the underlying type confusion instead of surfacing it. The fix is getValue() (§5.5), not loose equality.
Exception: x == null to test for both null and undefined is permitted and idiomatic. Do not use it if you can be more specific.
13.6 Quotes
Single quotes for strings. Double quotes only to avoid escaping.
var query = 'active=true';
var msg = "Don't do that";
Reason: ServiceNow script fields are frequently embedded in XML (sys_update_xml, update sets, sys_script.script). Single quotes reduce the escaping surface. It is also the ESLint default.
13.7 Operator spacing
Spaces around binary operators. No space after unary.
// Good
var total = base + tax;
var isReady = (count > 0) && isValid;
i++;
!isValid;
// Bad
var total = base+tax;
var isReady = (count>0)&&isValid;
i ++;
! isValid;
13.8 Commas and trailing commas
Comma at the end of the line. No trailing comma in object literals or arrays.
// Good
var config = {
batchSize: 500,
debug: false
};
// Bad — Rhino tolerates it, but older parsers and the ServiceNow XML
// round-trip do not. Not worth the risk.
var config = {
batchSize: 500,
debug: false,
};
13.9 Line length
Soft limit 120 characters. ServiceNow's script editor does not soft-wrap by default and horizontal scrolling in the platform UI is painful. Long encoded queries and long log concatenations are the usual offenders — break them.
gs.error('[GroupAuditEngine] run() failed for group ' + groupSysId +
': ' + e.message);
13.10 Blank lines
- One blank line between logical blocks inside a function.
- One blank line between methods in a prototype.
- No blank line immediately after
{or immediately before}. - No more than one consecutive blank line.
13.11 else placement
Cuddled. } else { on one line.
// Good
if (a) {
x();
} else {
y();
}
13.12 Method chaining
Break at the dot, aligned, when a chain exceeds one line.
var gaIncident = new GlideAggregate('incident');
gaIncident.addActiveQuery();
gaIncident.addAggregate('COUNT');
gaIncident.groupBy('assignment_group');
gaIncident.orderByAggregate('COUNT');
gaIncident.query();
Note that GlideRecord/GlideAggregate methods return undefined, not this. They are not chainable. Writing gr.addQuery('a', 1).addQuery('b', 2) throws. This is a common cross-language reflex and it does not work here.
13.13 The meta-rule
Any consistent style beats any inconsistent one. These conventions exist to pre-decide hundreds of trivial choices so that code review discusses logic, git blame stays meaningful, and diffs contain only semantic change. If a convention here conflicts with the enforced ESLint config, the config wins — and the config should be amended, not bypassed.
Appendix A: .eslintrc.json
Tuned for Rhino ES5 and ServiceNow Global scope. Drop this at the root of your sn-scriptsync folder.
{
"root": true,
"env": {
"browser": false,
"node": false
},
"parserOptions": {
"ecmaVersion": 5,
"sourceType": "script"
},
"globals": {
"gs": "readonly",
"GlideRecord": "readonly",
"GlideRecordSecure": "readonly",
"GlideAggregate": "readonly",
"GlideMultipleUpdate": "readonly",
"GlideDateTime": "readonly",
"GlideDate": "readonly",
"GlideDuration": "readonly",
"GlideSysAttachment": "readonly",
"GlideElement": "readonly",
"GlideFilter": "readonly",
"GlideEncrypter": "readonly",
"GlideStringUtil": "readonly",
"GlideDBFunctionBuilder": "readonly",
"GlideScopedEvaluator": "readonly",
"GlideTableHierarchy": "readonly",
"GlideSession": "readonly",
"GlideTransaction": "readonly",
"GlideProperties": "readonly",
"GlideQueryCondition": "readonly",
"Class": "readonly",
"AbstractAjaxProcessor": "readonly",
"JSUtil": "readonly",
"ArrayUtil": "readonly",
"j2js": "readonly",
"current": "writable",
"previous": "writable",
"g_form": "readonly",
"g_user": "readonly",
"g_scratchpad": "readonly",
"GlideAjax": "readonly",
"action": "readonly",
"sn_ws": "readonly",
"sn_cc": "readonly"
},
"rules": {
"no-var": "off",
"prefer-const": "off",
"prefer-arrow-callback": "off",
"prefer-template": "off",
"object-shorthand": "off",
"no-eval": "error",
"no-implied-eval": "error",
"no-new-func": "error",
"no-undef": "error",
"no-implicit-globals": "error",
"no-unused-vars": ["error", { "args": "none" }],
"no-empty": ["error", { "allowEmptyCatch": false }],
"no-console": "error",
"eqeqeq": ["error", "always", { "null": "ignore" }],
"curly": ["error", "all"],
"semi": ["error", "always"],
"quotes": ["error", "single", { "avoidEscape": true }],
"indent": ["error", 4, { "SwitchCase": 1 }],
"brace-style": ["error", "1tbs", { "allowSingleLine": false }],
"keyword-spacing": ["error", { "before": true, "after": true }],
"space-before-function-paren": ["error", {
"anonymous": "always",
"named": "never",
"asyncArrow": "always"
}],
"space-before-blocks": ["error", "always"],
"space-infix-ops": "error",
"space-unary-ops": ["error", { "words": true, "nonwords": false }],
"comma-style": ["error", "last"],
"comma-dangle": ["error", "never"],
"comma-spacing": ["error", { "before": false, "after": true }],
"one-var": ["error", "never"],
"vars-on-top": "error",
"max-len": ["warn", { "code": 120, "ignoreComments": false }],
"no-multiple-empty-lines": ["error", { "max": 1, "maxBOF": 0, "maxEOF": 1 }],
"padded-blocks": ["error", "never"],
"camelcase": ["error", { "properties": "never" }],
"new-cap": ["error", { "capIsNew": false }],
"no-loop-func": "error",
"no-throw-literal": "error",
"consistent-return": "error",
"radix": "error",
"no-with": "error",
"no-proto": "error",
"no-extend-native": "error"
}
}
Notes on the disabled rules: no-var, prefer-const, prefer-arrow-callback, prefer-template, and object-shorthand are all explicitly off because ES5 is the target. Leaving them on produces hundreds of unfixable warnings and trains the team to ignore the linter.
new-cap is set to capIsNew: false because Class.create() is capitalized but is not a constructor call.
radix is error because parseInt(x) without a radix has bitten enough people. Always parseInt(x, 10).
Appendix B: VS Code Snippets
Save as .vscode/servicenow.code-snippets in your sn-scriptsync folder.
{
"Script Include (Class.create)": {
"prefix": "snsi",
"body": [
"/**",
" * ${1:ClassName}",
" *",
" * ${2:Description}",
" *",
" * Scope: Global",
" * Owner: ${3:Platform COE}",
" */",
"var $1 = Class.create();",
"",
"$1.prototype = {",
"",
"\tinitialize: function () {",
"\t\t$0",
"\t},",
"",
"\ttype: '$1'",
"};"
],
"description": "ServiceNow Script Include boilerplate"
},
"Script Include (AJAX)": {
"prefix": "snajax",
"body": [
"var ${1:ClassName}Ajax = Class.create();",
"",
"$1Ajax.prototype = Object.extendsObject(AbstractAjaxProcessor, {",
"",
"\t${2:methodName}: function () {",
"\t\tvar ${3:param} = this.getParameter('sysparm_$3');",
"\t\t$0",
"\t},",
"",
"\ttype: '$1Ajax'",
"});"
],
"description": "Client-callable Script Include"
},
"Business Rule": {
"prefix": "snbr",
"body": [
"(function executeRule(current, previous /*null when async*/) {",
"",
"\t$0",
"",
"})(current, previous);"
],
"description": "Business Rule IIFE wrapper"
},
"GlideRecord query (single)": {
"prefix": "sngr1",
"body": [
"var gr${1:Table} = new GlideRecord('${2:table}');",
"gr$1.addQuery('${3:field}', ${4:value});",
"gr$1.setLimit(1);",
"gr$1.query();",
"if (gr$1.next()) {",
"\t$0",
"}"
],
"description": "Single-record GlideRecord query with setLimit"
},
"GlideRecord query (loop)": {
"prefix": "sngr",
"body": [
"var gr${1:Table} = new GlideRecord('${2:table}');",
"gr$1.addQuery('${3:field}', ${4:value});",
"gr$1.setColumns('${5:sys_id,name}');",
"gr$1.query();",
"while (gr$1.next()) {",
"\t$0",
"}"
],
"description": "GlideRecord loop with setColumns"
},
"GlideAggregate count": {
"prefix": "snga",
"body": [
"var ga${1:Table} = new GlideAggregate('${2:table}');",
"ga$1.addQuery('${3:field}', ${4:value});",
"ga$1.addAggregate('COUNT');",
"ga$1.query();",
"var ${5:count} = ga$1.next() ? parseInt(ga$1.getAggregate('COUNT'), 10) : 0;",
"$0"
],
"description": "GlideAggregate COUNT"
},
"Try/catch with logging": {
"prefix": "sntry",
"body": [
"try {",
"\t$0",
"} catch (e) {",
"\tgs.error('[${1:ArtifactName}] ${2:context}: ' + e.message + '\\n' + e.stack);",
"\tthrow e;",
"}"
],
"description": "try/catch with structured logging"
},
"JSDoc method": {
"prefix": "snjsdoc",
"body": [
"/**",
" * ${1:Description}",
" *",
" * @param {${2:string}} ${3:paramName} - ${4:Description}",
" * @returns {${5:type}} ${6:Description}",
" * @throws {Error} ${7:When...}",
" */"
],
"description": "JSDoc block for a Script Include method"
}
}
End of ServiceNow JavaScript Style Guide v1.1