‹ All essays

ServiceNow

JavaScript Inside ServiceNow: var, let, const, Types, and What Actually Runs Where

The first thing to know

ServiceNow runs two completely separate JavaScript environments. Code that works on one side will crash on the other.

Server-sideClient-side
Runs inServiceNow's Java-based server (Rhino engine)The user's web browser
Script typesBusiness Rules, Script Includes, Scheduled Jobs, Fix Scripts, Background ScriptsClient Scripts, UI Policies (scripted), UI Actions (client)
JS versionES5 (no let/const, no arrow functions, no template literals in global scope)Modern ES6+ (depends on the user's browser)
Database accessYes — GlideRecord, GlideAggregateNo — must use GlideAjax to call server
Available objectscurrent, previous, gs, GlideRecordg_form, g_user, g_list

Variables: var, let, const

Server-side (Business Rules, Script Includes)

The server uses the Rhino engine which only supports ES5 in global scope. That means:

// This works everywhere on the server:
var name = 'incident';
var count = 0;
var isActive = true;

// This FAILS in global-scope server scripts:
// let name = 'incident';    // SyntaxError
// const MAX = 100;          // SyntaxError
// var msg = `Hello ${name}`; // SyntaxError — no template literals

// EXCEPTION: Scoped applications run on a newer engine (ES2021+)
// In scoped apps, let/const/arrow functions DO work:
// let x = 10;  // works in scoped app scripts
// const y = 20; // works in scoped app scripts

Rule of thumb: In global scope, use var. In scoped applications, you can use let and const. When in doubt, use var — it works everywhere.

Client-side (Client Scripts)

Client scripts run in the browser, so modern JS works fine:

// All of these work in Client Scripts:
var oldWay = 'works';
let newWay = 'also works';
const CONSTANT = 'works too';
var msg = `Hello ${oldWay}`; // template literals work

var vs let — the actual difference

// var is function-scoped — it leaks out of blocks
for (var i = 0; i < 5; i++) {
    // i is visible here
}
gs.info(i); // 5 — var leaks out of the for loop

// let is block-scoped — it stays inside the block
for (let j = 0; j < 5; j++) {
    // j is visible here
}
// gs.info(j); // ReferenceError — j does not exist here

// const — like let but cannot be reassigned
const MAX_RETRIES = 3;
// MAX_RETRIES = 5; // TypeError — cannot reassign

Data types

// String
var desc = 'Server is down';
var desc2 = "Also a string";  // single or double quotes both work

// Number (integer and decimal are the same type)
var priority = 1;
var percent = 99.5;

// Boolean
var isActive = true;
var isClosed = false;

// Null and Undefined
var assigned = null;       // explicitly set to nothing
var notSet;                // undefined — never assigned a value
// gs.nil() checks for null, undefined, AND empty string:
gs.info(gs.nil(null));       // true
gs.info(gs.nil(undefined));  // true
gs.info(gs.nil(''));         // true
gs.info(gs.nil('hello'));    // false

// Array
var priorities = [1, 2, 3, 4];
gs.info(priorities[0]);     // 1
gs.info(priorities.length); // 4

// Object
var config = {
    table: 'incident',
    limit: 50,
    active: true
};
gs.info(config.table);      // 'incident'
gs.info(config['limit']);   // 50 — bracket notation also works

Type gotchas in ServiceNow

GlideRecord fields do not return plain JavaScript types. They return GlideElement objects. This causes bugs that are hard to spot.

var gr = new GlideRecord('incident');
gr.get('number', 'INC0010001');

// WRONG — comparing a GlideElement to a string
if (gr.priority == '1') {
    // This MIGHT work, but it is unreliable.
    // gr.priority is a GlideElement object, not a string.
}

// CORRECT — use getValue() to get the raw string value
if (gr.getValue('priority') == '1') {
    // This always works. getValue returns a plain string.
}

// WRONG — concatenating GlideElement into a string
var msg = 'Priority is ' + gr.priority;
// This calls toString() on the GlideElement, which usually works
// but can behave unexpectedly with reference fields.

// CORRECT — explicit getValue
var msg = 'Priority is ' + gr.getValue('priority');

// WRONG — storing a GlideElement in a variable for later use
var saved = gr.caller_id;
gr.next(); // moves to next record
gs.info(saved); // NOW POINTS TO THE NEW RECORD'S caller_id!
// GlideElement is a reference to the cursor position, not a copy.

// CORRECT — store the value, not the element
var saved = gr.getValue('caller_id');
gr.next();
gs.info(saved); // still has the original value

Functions

// Basic function (works everywhere)
function calculatePriority(impact, urgency) {
    if (impact == 1 && urgency == 1) return 1;
    if (impact == 1 || urgency == 1) return 2;
    return 3;
}
var p = calculatePriority(1, 2); // returns 2

// Function in a Business Rule (the wrapper pattern)
// ServiceNow wraps your BR code in an IIFE:
(function executeRule(current, previous) {
    // 'current' = the record being saved
    // 'previous' = the record before changes (null on insert)

    current.priority = calculatePriority(
        current.getValue('impact'),
        current.getValue('urgency')
    );
})(current, previous);

// Arrow functions — ONLY in scoped apps or client scripts
// var calc = (a, b) => a + b;  // fails in global server scope
// Use regular functions on the server in global scope.

Loops

// for loop — iterate a known number of times
for (var i = 0; i < 10; i++) {
    gs.info('Iteration ' + i);
}

// while loop — the GlideRecord pattern
var gr = new GlideRecord('incident');
gr.addQuery('active', true);
gr.setLimit(100);
gr.query();
while (gr.next()) {
    gs.info(gr.getValue('number'));
}
// gr.next() returns false when there are no more records

// for...in — iterate over object keys
var config = { table: 'incident', limit: 50 };
for (var key in config) {
    gs.info(key + ' = ' + config[key]);
}
// Output: table = incident, limit = 50

// Array iteration
var items = ['one', 'two', 'three'];
for (var i = 0; i < items.length; i++) {
    gs.info(items[i]);
}
// .forEach works in scoped apps and client scripts:
// items.forEach(function(item) { gs.info(item); });

Conditionals

// if / else if / else
var priority = gr.getValue('priority');
if (priority == '1') {
    gs.info('Critical');
} else if (priority == '2') {
    gs.info('High');
} else {
    gs.info('Normal or lower');
}

// == vs === (loose vs strict equality)
gs.info(1 == '1');   // true — loose comparison, converts types
gs.info(1 === '1');  // false — strict comparison, different types
// In ServiceNow, getValue() always returns strings, so:
// gr.getValue('priority') == 1   → true (loose, works)
// gr.getValue('priority') === 1  → false (strict, different types)
// gr.getValue('priority') === '1' → true (strict, both strings)
// Best practice: use == when comparing getValue output to numbers,
// or convert explicitly: Number(gr.getValue('priority')) === 1

// Ternary operator
var label = (priority == '1') ? 'Critical' : 'Not critical';

// Switch statement
switch (gr.getValue('state')) {
    case '1': gs.info('New'); break;
    case '2': gs.info('In Progress'); break;
    case '6': gs.info('Resolved'); break;
    case '7': gs.info('Closed'); break;
    default: gs.info('Other state');
}

Strings

var desc = 'Email server down in building A';

desc.length;                    // 33
desc.indexOf('server');         // 6 (position of first match)
desc.indexOf('xyz');            // -1 (not found)
desc.includes('server');        // true (scoped apps/client only)
desc.toLowerCase();             // 'email server down in building a'
desc.toUpperCase();             // 'EMAIL SERVER DOWN IN BUILDING A'
desc.substring(0, 5);          // 'Email'
desc.split(' ');                // ['Email', 'server', 'down', 'in', 'building', 'A']
desc.replace('down', 'offline'); // 'Email server offline in building A'
desc.trim();                    // removes leading/trailing whitespace

// String concatenation
var msg = 'Incident ' + gr.getValue('number') + ' is priority ' + gr.getValue('priority');

// Template literals (scoped apps and client scripts only)
// var msg = `Incident ${number} is priority ${priority}`;

Arrays

var groups = ['Network', 'Database', 'Security'];

groups.length;          // 3
groups[0];              // 'Network'
groups.push('Storage'); // adds to end: ['Network','Database','Security','Storage']
groups.pop();           // removes last: ['Network','Database','Security']
groups.indexOf('Database'); // 1
groups.join(', ');      // 'Network, Database, Security'

// Check if value exists in array
if (groups.indexOf('Network') !== -1) {
    gs.info('Found it');
}

// Build an array from GlideRecord results
var numbers = [];
var gr = new GlideRecord('incident');
gr.addQuery('priority', 1);
gr.setLimit(10);
gr.query();
while (gr.next()) {
    numbers.push(gr.getValue('number'));
}
gs.info('P1 incidents: ' + numbers.join(', '));

Objects / JSON

// Create an object
var incident = {
    number: 'INC0010001',
    priority: 1,
    active: true,
    tags: ['network', 'vpn']
};

// Access properties
gs.info(incident.number);       // 'INC0010001'
gs.info(incident['priority']);  // 1

// Add/modify properties
incident.state = 'New';
incident.priority = 2;

// Convert to JSON string (for logging, REST payloads, etc.)
var jsonStr = JSON.stringify(incident);
gs.info(jsonStr);
// {"number":"INC0010001","priority":2,"active":true,"tags":["network","vpn"],"state":"New"}

// Parse JSON string back to object
var parsed = JSON.parse(jsonStr);
gs.info(parsed.number); // 'INC0010001'

// Common use: REST API responses
var response = new sn_ws.RESTMessageV2('MyAPI', 'GET');
var body = response.execute().getBody();
var data = JSON.parse(body);  // parse response body into usable object

Error handling

// try/catch — prevent your script from crashing the entire transaction
try {
    var gr = new GlideRecord('incident');
    gr.get('number', 'INC0010001');
    var result = JSON.parse(gr.getValue('u_custom_json_field'));
    gs.info(result.key);
} catch (e) {
    gs.error('Failed to parse JSON: ' + e.message);
    // The script continues instead of crashing
}

// try/catch/finally
try {
    // risky operation
} catch (e) {
    gs.error('Error: ' + e.message);
} finally {
    // runs no matter what — cleanup code goes here
}

// When to use try/catch:
// - Parsing JSON from external systems (malformed data)
// - REST API calls (network failures)
// - Any operation where bad data could crash your Business Rule

Scope — the #1 confusion point

// Variables declared with var inside a function are LOCAL to that function
function myFunction() {
    var localVar = 'only exists here';
    gs.info(localVar); // works
}
// gs.info(localVar); // ReferenceError — does not exist outside

// Variables declared outside any function are GLOBAL to the script
var globalVar = 'visible everywhere in this script';
function test() {
    gs.info(globalVar); // works — can see the outer variable
}

// In Business Rules, 'current' and 'previous' are provided by the platform.
// You do not declare them. They are injected into the script context:
(function executeRule(current, previous) {
    // current = the record being modified
    // previous = the record BEFORE your changes (null on insert)
    // Both are GlideRecord objects pointing to the current row
})(current, previous);

What does NOT work on the server

// These are browser/Node.js features. They do not exist on the ServiceNow server.

// setTimeout / setInterval — no timers on server
// setTimeout(function() { }, 1000); // does not exist

// console.log — use gs.info instead
// console.log('test'); // does not exist

// document / window / DOM — there is no browser on the server
// document.getElementById('test'); // does not exist

// fetch / XMLHttpRequest — use RESTMessageV2 instead
// fetch('https://api.example.com'); // does not exist

// require / import — use gs.include for Script Includes
// var lib = require('mylib'); // does not exist

// Promise / async / await — not available in global scope server scripts
// Use GlideHTTPRequest or synchronous REST calls

Quick reference: where to write each script type

I want to...Write a...Navigate to
Validate data before saveBefore Business RuleSystem Definition > Business Rules
Create related records after saveAfter Business RuleSystem Definition > Business Rules
Show/hide fields on a formClient Script (onLoad/onChange) or UI PolicySystem Definition > Client Scripts
Validate before form submitClient Script (onSubmit)System Definition > Client Scripts
Reusable server functionScript IncludeSystem Definition > Script Includes
Call server from clientGlideAjax + Script Include (extends AbstractAjaxProcessor)Both
Run something on a scheduleScheduled JobSystem Definition > Scheduled Jobs
One-time data cleanupFix Script or Background ScriptSystem Definition > Fix Scripts, or Scripts - Background
Custom button on a formUI ActionSystem Definition > UI Actions