What It Is
UI Scripts are reusable JavaScript libraries that execute entirely in the browser, serving as the foundation for sharing common functionality across multiple Client Scripts. They solve the fundamental problem of code duplication in client-side ServiceNow development—without them, you'd be copying and pasting the same validation logic, utility functions, and API calls across dozens of form scripts. ServiceNow loads UI Scripts automatically into the browser context whenever a form loads, making their functions immediately available to any Client Script, UI Policy, or Catalog Client Script on that page.
Architecturally, UI Scripts sit exclusively on the client side—they never execute on the ServiceNow server. When a user loads a form, ServiceNow bundles all applicable UI Scripts into the page's JavaScript payload, where they become part of the browser's execution context alongside jQuery, Angular controllers, and ServiceNow's native client APIs like g_form and g_user. This browser-only execution means UI Scripts have access to the DOM, can manipulate form fields directly, and can make AJAX calls back to the server, but they cannot directly query database tables or execute server-side APIs.
Under the hood, ServiceNow processes UI Scripts during the form rendering pipeline. When you navigate to a record, ServiceNow examines the form's application scope, identifies all UI Scripts marked as Global or belonging to that application, and injects them as <script> tags in the page head before any Client Scripts execute. The platform respects the dependency order you define, ensuring that foundational libraries load before scripts that depend on them. This mechanism is why UI Script functions are immediately available in Client Scripts without any import statements—they're already loaded into the global JavaScript namespace.
Without UI Scripts, enterprise ServiceNow implementations become unmaintainable disasters of duplicated code. Every form that needs to validate phone numbers would contain its own copy of the validation logic. Every Client Script that formats currency would repeat the same formatting function. When business rules change—and they always do—you'd be hunting through hundreds of Client Scripts to update the same logic copy-pasted everywhere. UI Scripts eliminate this technical debt by centralizing common functionality into tested, reusable libraries that can be updated in one place and immediately affect all dependent forms.
Developers use UI Scripts daily for building form interactions, client-side validations, and AJAX utilities. System administrators rely on them indirectly when implementing UI Policies or configuring catalog items that need custom client-side behavior. Solution architects treat UI Scripts as critical infrastructure components, designing them as stable APIs that multiple development teams can consume without coupling their implementations. In scoped application development, UI Scripts become even more crucial as they define the client-side boundaries between applications while enabling controlled sharing of functionality.
UI Scripts relate most closely to Script Includes and Client Scripts, forming a three-tier architecture for ServiceNow customization. While Script Includes provide server-side reusable logic and Client Scripts handle form-specific interactions, UI Scripts occupy the middle ground of client-side reusable logic. They also complement CSS Include Records for complete UI customization—UI Scripts handle the behavior while CSS Includes control the appearance. Unlike Business Rules or Workflows that respond to database events, UI Scripts only execute in response to user interface interactions, making them essential for creating responsive, interactive forms that don't require server round-trips for every user action.
How It Works Under the Hood
ServiceNow's UI Script execution follows a precise loading sequence that most developers never see but absolutely need to understand. When a user requests a form, ServiceNow queries the sys_ui_script table to identify all active UI Scripts that match the current application scope or are marked as Global. The platform then sorts these scripts by their Order field and any dependency relationships, ensuring that foundational libraries load before dependent scripts. This dependency resolution happens server-side during page composition, not in the browser.
Once ServiceNow determines the loading order, it concatenates all applicable UI Script code into the form's JavaScript payload. This concatenation includes automatic scope wrapping for security—each UI Script executes within its own closure to prevent variable pollution between scripts. ServiceNow injects these compiled scripts as inline JavaScript in the page head, making them available to the global namespace before any form-specific JavaScript executes. The platform also handles caching at this stage, storing compiled script bundles to avoid reprocessing on subsequent page loads.
The Loading Lifecycle
- User navigates to a form URL, triggering ServiceNow's form rendering pipeline on the server
- ServiceNow identifies the form's application scope and queries
sys_ui_scriptfor matching active UI Scripts - Platform sorts UI Scripts by Order field and resolves any dependencies between scripts
- ServiceNow wraps each UI Script in security closures and concatenates them into a single JavaScript payload
- Compiled UI Script bundle gets injected into the page head as inline
<script>tags - Browser executes UI Scripts in dependency order, making their functions available globally
- Form loads completely, Client Scripts execute and can immediately call UI Script functions
Enjoying this? Get one deep-dive per week.
Join 1,000+ ServiceNow pros — scripts, GlideRecord patterns, Flow Designer techniques, and career moves. Free.
The Core Pattern
// Global UI Script providing reusable form utilities
// Namespace prevents conflicts with other scripts
var FormUtilities = {
// Validate email format using regex pattern
validateEmail: function(email) {
var emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailPattern.test(email);
},
// Set field mandatory with custom styling
setMandatoryField: function(fieldName, isMandatory) {
g_form.setMandatory(fieldName, isMandatory);
// Add visual indicator for better UX
if (isMandatory) {
g_form.getControl(fieldName).style.backgroundColor = '#fff2cc';
} else {
g_form.getControl(fieldName).style.backgroundColor = '';
}
},
// Show confirmation dialog with consistent styling
confirmAction: function(message, callback) {
var confirmed = confirm(message);
if (confirmed && typeof callback === 'function') {
callback();
}
return confirmed;
}
};function onLoad() {
// UI Script functions are immediately available
// No import or initialization required
// Use utility to set mandatory fields based on priority
var priority = g_form.getValue('priority');
if (priority == '1' || priority == '2') {
FormUtilities.setMandatoryField('assigned_to', true);
FormUtilities.setMandatoryField('assignment_group', true);
}
}
function onChange(control, oldValue, newValue, isLoading) {
if (isLoading || newValue == '') return;
// Validate email fields using shared utility
if (control == 'caller_id.email') {
if (!FormUtilities.validateEmail(newValue)) {
g_form.addErrorMessage('Please enter a valid email address');
g_form.clearValue('caller_id.email');
}
}
}Always use namespace objects in UI Scripts to avoid polluting the global scope. Multiple UI Scripts loading on the same form can overwrite each other's functions without namespacing.
Real-World Scenarios
Dynamic Field Population from External APIs
HR needs to populate employee location and department data from an external HRIS system when creating new user records. The lookup needs to be reusable across multiple forms and handle network timeouts gracefully.
var HRISIntegration = {
// Populate user details from external HRIS API
populateEmployeeData: function(employeeId, callback) {
if (!employeeId) {
callback(null, 'Employee ID is required');
return;
}
var ga = new GlideAjax('HRISScriptInclude');
ga.addParam('sysparm_name', 'getEmployeeDetails');
ga.addParam('sysparm_employee_id', employeeId);
// Set timeout to prevent hanging forms
ga.getXMLAnswer(function(response) {
try {
var employeeData = JSON.parse(response);
if (employeeData.success) {
callback(employeeData.result, null);
} else {
callback(null, employeeData.error || 'Failed to retrieve employee data');
}
} catch (e) {
callback(null, 'Invalid response from HRIS system');
}
}, 5000); // 5 second timeout
}
};var HRISScriptInclude = Class.create();
HRISScriptInclude.prototype = Object.extendsObject(AbstractAjaxProcessor, {
getEmployeeDetails: function() {
var employeeId = this.getParameter('sysparm_employee_id');
var result = { success: false, result: null, error: null };
try {
// Call external HRIS REST endpoint
var request = new sn_ws.RESTMessageV2('HRIS_Employee_Lookup', 'GET');
request.setStringParameterNoEscape('employee_id', employeeId);
var response = request.execute();
if (response.getStatusCode() == 200) {
var employeeData = JSON.parse(response.getBody());
result.success = true;
result.result = {
location: employeeData.location_code,
department: employeeData.dept_name,
manager: employeeData.manager_email
};
} else {
result.error = 'HRIS system returned status: ' + response.getStatusCode();
}
} catch (e) {
result.error = 'Error connecting to HRIS: ' + e.message;
}
return JSON.stringify(result);
},
type: 'HRISScriptInclude'
});Watch for AJAX timeouts in UI Scripts—users will click submit buttons while background requests are still processing, creating race conditions. Always implement proper error handling and user feedback for external API calls. The callback pattern shown here prevents form submission until the lookup completes or fails explicitly.
Complex Field Validation with Business Rules
Finance requires purchase requisitions to validate budget codes against department allocations and fiscal year constraints before submission. The validation logic needs to be consistent across standard forms, mobile interfaces, and service catalog items.
var BudgetValidation = {
// Validate budget code against department and fiscal constraints
validateBudgetAllocation: function(budgetCode, department, amount, callback) {
if (!budgetCode || !department || !amount) {
callback({ valid: false, message: 'Budget code, department, and amount are required' });
return;
}
var ga = new GlideAjax('BudgetValidationProcessor');
ga.addParam('sysparm_name', 'validateAllocation');
ga.addParam('sysparm_budget_code', budgetCode);
ga.addParam('sysparm_department', department);
ga.addParam('sysparm_amount', amount);
ga.getXMLAnswer(function(response) {
var validation = JSON.parse(response);
// Show specific error messaging for different failure types
if (!validation.valid) {
g_form.addErrorMessage('Budget Validation: ' + validation.message);
g_form.flash('budget_code', '#ff6b6b', 3000);
}
callback(validation);
});
},
// Real-time budget balance display
displayRemainingBudget: function(budgetCode, targetField) {
// Implementation for showing remaining budget in real-time
}
};var BudgetValidationProcessor = Class.create();
BudgetValidationProcessor.prototype = Object.extendsObject(AbstractAjaxProcessor, {
validateAllocation: function() {
var budgetCode = this.getParameter('sysparm_budget_code');
var department = this.getParameter('sysparm_department');
var amount = parseFloat(this.getParameter('sysparm_amount'));
var result = { valid: false, message: '', remainingBudget: 0 };
// Query budget allocation table
var budgetGR = new GlideRecord('x_finance_budget_allocation');
budgetGR.addQuery('code', budgetCode);
budgetGR.addQuery('department', department);
budgetGR.addQuery('fiscal_year', this.getCurrentFiscalYear());
budgetGR.query();
if (!budgetGR.next()) {
result.message = 'Budget code ' + budgetCode + ' not found for department';
return JSON.stringify(result);
}
var allocated = parseFloat(budgetGR.getValue('allocated_amount'));
var spent = this.getSpentAmount(budgetCode, department);
var remaining = allocated - spent;
if (amount > remaining) {
result.message = 'Insufficient budget. Remaining: $' + remaining.toFixed(2);
} else {
result.valid = true;
result.remainingBudget = remaining - amount;
}
return JSON.stringify(result);
}
});Complex validations like this often fail when users rapidly change field values because multiple AJAX requests overlap. Implement request queuing or debouncing in your UI Script to prevent validation race conditions. Also consider caching budget data client-side for frequently accessed budget codes to reduce server load.
Progressive Form Enhancement with Conditional Logic
IT needs incident forms that dynamically show different field sets based on category selection, but also remember user preferences and pre-populate related fields from previous tickets. The enhancement must work seamlessly across desktop and mobile interfaces.
var IncidentFormEnhancer = {
// Category-based field visibility with user preference memory
enhanceFormByCategory: function(category) {
var categoryConfig = this.getCategoryConfiguration(category);
var userPrefs = this.getUserPreferences();
// Hide all conditional sections first
this.hideAllConditionalSections();
// Show fields specific to selected category
if (categoryConfig.requiredSections) {
categoryConfig.requiredSections.forEach(function(section) {
g_form.setSectionDisplay(section, true);
// Apply user's preferred field order if available
if (userPrefs.fieldOrder && userPrefs.fieldOrder[section]) {
this.applyFieldOrdering(section, userPrefs.fieldOrder[section]);
}
});
}
// Pre-populate fields from user's recent tickets
this.populateFromHistory(category);
// Set dynamic mandatory fields based on category
this.setCategoryMandatoryFields(categoryConfig.mandatoryFields);
},
// Smart field population from user's ticket history
populateFromHistory: function(category) {
// Implementation for historical data population
}
};Mobile forms have different DOM structures than desktop forms. Test your UI Script enhancements on both interfaces—field manipulation methods that work perfectly on desktop often fail silently on mobile.
The Classic Mistake
Using UI Scripts to make server-side API calls without proper error handling and loading states.
// UI Script: UserUtils
function getUserDetails(userSysId) {
var ga = new GlideAjax('UserDetailsAjax');
ga.addParam('sysparm_name', 'getUserInfo');
ga.addParam('sysparm_user_id', userSysId);
var result;
ga.getXMLWait();
if (ga.getAnswer()) {
result = JSON.parse(ga.getAnswer());
}
return result;
}
// Called from Client Script
var userInfo = getUserDetails(g_user.userID);
g_form.setValue('assigned_to_name', userInfo.name);This fails because getXMLWait() blocks the browser thread, causing timeouts and unresponsive UI. The browser console shows "Synchronous XMLHttpRequest on the main thread is deprecated" warnings, and users see frozen forms. ServiceNow's client-side security model prevents synchronous calls from working reliably in Service Portal and mobile apps. When the AJAX call fails, result remains undefined, causing downstream JavaScript errors.
// UI Script: UserUtils
function getUserDetails(userSysId, callback, errorCallback) {
var ga = new GlideAjax('UserDetailsAjax');
ga.addParam('sysparm_name', 'getUserInfo');
ga.addParam('sysparm_user_id', userSysId);
ga.getXML(function(response) {
try {
var answer = response.responseXML.documentElement.getAttribute('answer');
if (answer) {
callback(JSON.parse(answer));
} else {
errorCallback('No data returned');
}
} catch (e) {
errorCallback('Parse error: ' + e.message);
}
});
}
// Called from Client Script with proper callbacks
getUserDetails(g_user.userID,
function(userInfo) { g_form.setValue('assigned_to_name', userInfo.name); },
function(error) { g_form.addErrorMessage('Failed to load user: ' + error); }
);Never use getXMLWait() in UI Scripts. Always use asynchronous getXML() with proper callback functions for success and error handling.
Performance Rules
- Keep UI Script functions under 50KB total size. Loading scripts over 100KB causes noticeable page load delays and mobile browser crashes on older devices.
- Never perform DOM manipulation with over 200 elements in a single UI Script function. Browser performance degrades exponentially, causing 5+ second freezes that trigger user complaints.
- Limit
GlideAjaxcalls to maximum 3 concurrent requests. More than 5 simultaneous AJAX calls from UI Scripts trigger ServiceNow's rate limiting, causing 429 HTTP errors. - Cache expensive calculations using
sessionStorageor global variables. Recalculating complex data structures on every form field change causes CPU spikes visible in browser performance monitoring. - Avoid
setInterval()with intervals shorter than 5 seconds in UI Scripts. Faster polling creates memory leaks and battery drain on mobile devices, leading to app store rejection. - Use
try-catchblocks around all JSON parsing and external API calls. Unhandled exceptions in UI Scripts break all subsequent Client Scripts on the same page, requiring full page refresh. - Minimize use of
eval()and dynamic function creation. These bypass JavaScript engine optimizations and trigger Content Security Policy violations in modern ServiceNow instances. - Keep global namespace pollution to under 10 function names. More than 15 global functions from UI Scripts cause naming conflicts with ServiceNow's core JavaScript libraries and third-party plugins.
Side Effects & Platform Behavior
- UI Scripts get cached in the browser and CDN for 24 hours. Changes don't appear until cache expires or admin forces cache refresh via
System Properties > Cache Management. - JavaScript errors in UI Scripts write to the
sys_ui_scripttable'ssys_mod_countfield gets incremented on every access, affecting system performance metrics. - Scoped UI Scripts create a separate JavaScript namespace. Functions aren't accessible across application scopes without explicit global declarations or
window.scopeNameprefixing. - UI Scripts load before Client Scripts but after UI Policies. This loading order affects function availability and can cause "function not defined" errors in Client Script onChange handlers.
- Mobile app and Service Portal have different JavaScript execution contexts. UI Scripts using
top.windowor iframe-specific methods fail silently in these environments. - Update Set exports include UI Script dependencies but not their execution order. Importing UI Scripts can break functionality if dependent scripts load before their libraries.
- UI Scripts with
console.log()statements create permanent browser console entries. This affects debugging and can expose sensitive information in production instances. - Global UI Scripts affect all users and applications. Memory leaks or infinite loops in global scripts impact entire instance performance and require immediate admin intervention.
- UI Scripts bypass normal ACL checking since they execute in browser context. Sensitive data validation must happen server-side through Script Includes called via
GlideAjax. - Browser developer tools can modify UI Script functions in real-time. Production instances should monitor for client-side script tampering through Content Security Policy headers.
Debugging When It Breaks
Most UI Script failures manifest as "function is not defined" errors in the browser console or silent failures where expected functionality simply doesn't work. Users see forms that don't populate fields, buttons that don't respond, or validation that never triggers. The browser's JavaScript console (F12 > Console) is your primary debugging tool - look for red error messages that reference your function names or show "Uncaught ReferenceError" exceptions.
ServiceNow logs UI Script execution errors in System Log > All with source "UI Script" but only for server-side compilation errors, not runtime JavaScript failures. For runtime issues, check the Network tab in browser dev tools for failed script loading (404 errors) or examine the Sources tab to verify your script loaded correctly. Common error patterns include "Mixed Content" warnings when UI Scripts make HTTP requests from HTTPS pages, and "Cross-Origin" errors when accessing external APIs without proper CORS configuration.
Quick diagnostic checklist:
- Verify UI Script is Active and has no syntax errors in the Script field
- Check Application scope matches the calling Client Script's scope
- Confirm Global checkbox is checked if script needs cross-scope access
- Test with browser cache disabled (Dev Tools > Settings > Disable cache)
- Validate function names don't conflict with ServiceNow core JavaScript libraries
Quick Reference
- UI Scripts load in alphabetical order by name, not creation order - prefix with numbers for dependency management
- Use
typeof functionName !== 'undefined'to check function availability before calling - Global UI Scripts appear in ALL scopes; scoped scripts only appear in their application scope and Global
- ServiceNow compresses UI Scripts in production - avoid relying on specific whitespace or formatting in
toString()operations - Mobile apps cache UI Scripts locally - script changes require app restart or cache clear to take effect
- Use
window.setTimeout()notsetTimeout()to avoid scope conflicts in scoped applications - Functions declared with
var functionName = function()aren't hoisted - usefunction functionName()for global availability - UI Scripts can't access
gs,current, or other server-side objects - useGlideAjaxfor server data - Clone UI Scripts across instances using XML export - Update Sets can miss dependency relationships
- Always wrap UI Script code in immediately invoked function expressions (IIFE) to prevent global variable pollution:
(function(){ /* your code */ })()