⚙️ ServiceNow — Scripts & Business Logic
ServiceNow Developer Guide · Digital Kimya
ServiceNow scripting runs in two distinct environments that never share memory directly: the server (Nashorn/Rhino JavaScript engine) and the client (the user's browser). Understanding which side your code runs on — and how to bridge between them — is the most fundamental skill in ServiceNow development.
Diagramme interactif — Server vs Client
The Two Environments
┌─────────────────────────────┐ ┌────────────────────────────────┐
│ SERVER (Nashorn/Rhino) │ │ CLIENT (Browser) │
│ │ │ │
│ Business Rule │ │ Client Script │
│ Script Include │◄─GlideAjax─►GlideAjax (caller) │
│ GlideRecord API │ │ UI Policy │
│ Scheduled Job │ │ UI Action (client-side) │
│ Notification / Email Event │ │ UI Macro / UI Page │
│ Inbound Action │ │ Catalog Client Script │
└─────────────────────────────┘ └────────────────────────────────┘Rule of thumb: if it touches the database, it's server-side. If it changes what the user sees without a page reload, it's client-side.
Server-Side Scripts
Business Rule
A Business Rule (BR) is a server-side script triggered by a database operation on a specific table. It is the most powerful and most abused script type in ServiceNow.
Four trigger timings:
| Timing | When it runs | Can modify values? | Blocks the user? |
|---|---|---|---|
| Before | Before the record is written to DB | ✅ Yes — changes are saved | ✅ Yes |
| After | After the record is written | ❌ No (must use .update()) | ✅ Yes |
| Async | After write, in background thread | ❌ No (must use .update()) | ❌ No |
| Display | Before a form is rendered (read operations) | N/A — scratchpad only | ✅ Yes |
Best practice: use async whenever you don't need to block the user's operation. Heavy processing (emails, integrations, complex queries) should always be async.
Example — Before Insert:
// Business Rule: "Auto-set Priority" — Before Insert on incident
// Condition: current.impact == 1 && current.urgency == 1
(function executeRule(current, previous) {
current.priority = 2; // High priority
current.assignment_group = 'a1b2c3d4...'; // Network Ops sys_id
})(current, previous);Example — Async After Update:
// Business Rule: "Notify Manager on P1" — Async, After on incident
// Condition: current.priority == 1 && current.priority.changes()
(function executeRule(current, previous) {
var evt = gs.eventQueue('incident.p1.created', current, current.caller_id.manager, gs.getUserName());
})(current, previous);Script Include
A Script Include is a reusable JavaScript library on the server. Instead of duplicating logic across multiple Business Rules, you centralise it in a Script Include and call it from anywhere server-side.
Key properties:
| Property | Value |
|---|---|
| Accessible from | Business Rules, other Script Includes, REST APIs |
| Client-accessible | Only via GlideAjax (must extend AbstractAjaxProcessor) |
| Scope | Global or Scoped App |
Example — Utility class:
// Script Include: IncidentUtils
var IncidentUtils = Class.create();
IncidentUtils.prototype = {
initialize: function() {},
getPriority: function(impact, urgency) {
// Priority matrix: impact 1-3, urgency 1-3
var matrix = {
'1-1': 1, '1-2': 2, '1-3': 3,
'2-1': 2, '2-2': 3, '2-3': 4,
'3-1': 3, '3-2': 4, '3-3': 5
};
return matrix[impact + '-' + urgency] || 5;
},
isVIP: function(userId) {
var user = new GlideRecord('sys_user');
user.get(userId);
return user.getValue('vip') === 'true';
},
type: 'IncidentUtils'
};
// Calling it from a Business Rule:
var utils = new IncidentUtils();
var priority = utils.getPriority(current.impact, current.urgency);
current.priority = priority;GlideRecord API
GlideRecord is the primary database API in ServiceNow. Every read, write, create, and delete operation goes through it server-side.
Reading records:
// Query all incidents in state "New" (1)
var gr = new GlideRecord('incident');
gr.addQuery('state', 1);
gr.addQuery('priority', '<=', 2); // P1 and P2 only
gr.orderByDesc('opened_at');
gr.setLimit(100);
gr.query();
while (gr.next()) {
gs.info('Incident: ' + gr.number + ' — ' + gr.short_description);
}Creating a record:
var gr = new GlideRecord('incident');
gr.initialize();
gr.short_description = 'Network outage — Building A';
gr.impact = 1;
gr.urgency = 1;
gr.caller_id = gs.getUserID();
var sysId = gr.insert(); // Returns sys_id of new record
gs.info('Created: ' + sysId);Updating records:
// Bulk update: close all resolved incidents older than 5 days
var gr = new GlideRecord('incident');
gr.addQuery('state', 6); // Resolved
gr.addQuery('resolved_at', '<', gs.daysAgo(5));
gr.query();
while (gr.next()) {
gr.state = 7; // Closed
gr.close_notes = 'Auto-closed after 5 days resolved';
gr.update();
}Common GlideRecord pitfalls:
| Mistake | Problem | Fix |
|---|---|---|
No .query() before .next() | No results | Always call .query() first |
| Direct string concatenation in queries | SQL injection risk | Use addQuery() |
Looping .update() on 10k records | Times out, performance issue | Use GlideRecord in Background Scripts or Scheduled Jobs |
gr.field vs gr.getValue('field') | .field returns GlideElement object | Use .getValue() for strings |
Scheduled Job
A Scheduled Job runs a server-side script on a CRON schedule — no user interaction required.
// Scheduled Job: "Auto-close Resolved Incidents"
// Schedule: Daily at 02:00
var gr = new GlideRecord('incident');
gr.addQuery('state', 6); // Resolved
gr.addQuery('resolved_at', '<', gs.daysAgo(5));
gr.query();
var count = 0;
while (gr.next()) {
gr.state = 7; // Closed
gr.close_notes = 'Auto-closed: resolved for 5+ days';
gr.update();
count++;
}
gs.log('Auto-close job completed: ' + count + ' incidents closed', 'AutoCloseJob');Use cases: nightly data cleanup, SLA escalation checks, scheduled reports, integration polling.
Client-Side Scripts
Client Script
Client Scripts run in the user's browser — they fire on form events without a server round-trip.
| Trigger | When | Use case |
|---|---|---|
onLoad | Form first renders | Set field defaults, hide sections |
onChange | A specific field value changes | Clear dependent fields, show/hide logic |
onSubmit | User clicks Submit | Validate before save, confirm dialogs |
Example — onChange:
// Client Script: Clear subcategory when category changes
function onChange(control, oldValue, newValue, isLoading) {
if (isLoading) return; // Don't run on initial load
g_form.clearValue('subcategory');
g_form.clearOptions('subcategory');
}Example — onLoad:
// Client Script: Hide resolution fields until state = Resolved
function onLoad() {
var state = g_form.getValue('state');
var isResolved = (state == '6');
g_form.setVisible('resolve_time', isResolved);
g_form.setVisible('close_notes', isResolved);
}UI Policy
UI Policy is the no-code alternative to Client Scripts for simple show/hide/mandatory logic. Always prefer UI Policy over Client Script for these cases — it's faster to configure, easier to audit, and doesn't require JavaScript knowledge.
| Use case | UI Policy | Client Script |
|---|---|---|
| Make field mandatory based on state | ✅ Preferred | Overkill |
| Hide a field based on category | ✅ Preferred | Overkill |
| Clear a field when another changes | ❌ Not supported | ✅ Required |
| Complex multi-field conditional logic | ❌ Limited | ✅ Required |
| Fetch data from server | ❌ Not possible | ✅ Via GlideAjax |
Example — UI Policy:
- Table:
incident - Condition:
state is Resolved - Actions:
resolution_notes→ Mandatory=true, Visible=true
GlideAjax — The Client-Server Bridge
GlideAjax is how client-side code calls server-side logic without a page reload. The client instantiates a GlideAjax object pointing to a Script Include, calls a method, and receives the response in an async callback.
Server-side (Script Include — must extend AbstractAjaxProcessor):
// Script Include: LocationLookupAjax
var LocationLookupAjax = Class.create();
LocationLookupAjax.prototype = Object.extendsObject(AbstractAjaxProcessor, {
getBuilding: function() {
var userId = this.getParameter('sysparm_user_id');
var user = new GlideRecord('sys_user');
user.get(userId);
return user.getValue('building');
},
type: 'LocationLookupAjax'
});Client-side (Client Script):
// Client Script: Auto-fill location from caller
function onChange(control, oldValue, newValue, isLoading) {
if (isLoading || !newValue) return;
var ga = new GlideAjax('LocationLookupAjax');
ga.addParam('sysparm_name', 'getBuilding');
ga.addParam('sysparm_user_id', newValue);
ga.getXMLAnswer(function(answer) {
g_form.setValue('location', answer);
});
}Why GlideAjax matters: direct GlideRecord calls are not available in Client Scripts — the browser has no access to the database. GlideAjax is the only safe, supported bridge.
Implementation Guide
ServiceNow Scripting — Decision Framework
Click any step to expand · 5 steps
Script Type Quick Reference
| Need | Use | Side |
|---|---|---|
| Run code when record is saved | Business Rule (before/after) | Server |
| Run code without blocking user | Business Rule (async) | Server |
| Reuse logic across multiple BRs | Script Include | Server |
| Read/write database records | GlideRecord API | Server |
| Run code on a schedule | Scheduled Job | Server |
| Send emails based on events | Notification + Email Event | Server |
| React to inbound email | Inbound Action | Server |
| Change form when field changes | Client Script (onChange) | Client |
| Validate on submit | Client Script (onSubmit) | Client |
| Show/hide/mandatory — simple rules | UI Policy | Client |
| Call server from client | GlideAjax | Bridge |
| Custom buttons / links | UI Action | Both |
Part of the Digital Kimya (opens in a new tab) ServiceNow Guide Series — Identity & Access · Tables & Data · Deployment