Client Scripts
SkillDev toolsComplete guide to ServiceNow client script development including onLoad, onChange, onSubmit, onCellEdit types, g_form API, GlideAjax server calls, and performance optimization
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Client Scripts skill
What this skill tells your AI
The instructions your AI receives, as published by happy-technologies-llc/happy-platform-skills in skills/development/client-scripts/SKILL.md and read by ahel’s review.
Overview
This skill covers comprehensive client script development in ServiceNow:
- Client script types: onLoad, onChange, onSubmit, onCellEdit
- The g_form API for form manipulation
- g_user and g_scratchpad objects for session context
- GlideAjax for asynchronous server calls
- Performance optimization and best practices
- Mobile/Service Portal considerations
- Debugging techniques and common patterns
When to use: When you need to execute JavaScript in the browser to manipulate form behavior, validate data, or provide dynamic user interactions.
Who should use this: Developers building custom form behaviors, validations, and user experience enhancements.
Prerequisites
- Roles:
client_script_adminoradmin - Access: sys_script_client, sys_ui_script tables
- Knowledge: JavaScript fundamentals, ServiceNow form architecture
- Related Skills:
catalog/ui-policiesfor simpler show/hide/mandatory logic
Understanding Client Scripts
Script Type Comparison
| Type | Trigger | Use Case | Performance Impact |
|---|---|---|---|
| onLoad | Form loads | Initialize fields, set defaults | Medium |
| onChange | Field value changes | Field dependencies, cascading logic | Low-Medium |
| onSubmit | Form submission | Validation, confirmation | Low |
| onCellEdit | List cell edited | List editing validation | Low |
Client Script vs UI Policy
┌─────────────────────────────────────────────────────────────┐
│ Decision Matrix │
├─────────────────────────────────────────────────────────────┤
│ Need to show/hide/mandatory fields? │
│ YES → Use UI Policy (no code, easier maintenance) │
│ │
│ Need to set field values or complex logic? │
│ YES → Use Client Script │
│ │
│ Need server-side data? │
│ YES → Use Client Script with GlideAjax │
│ │
│ Need to prevent form submission? │
│ YES → Use Client Script (onSubmit) │
│ │
│ Simple field validation? │
│ Dictionary validation → UI Policy → Client Script │
└─────────────────────────────────────────────────────────────┘
Execution Order
Form Load Sequence:
1. UI Policies (on load = true) evaluate
2. onLoad Client Scripts execute (by order)
3. Default values applied
4. Field-level ACLs applied
Field Change Sequence:
1. onChange Client Script for field executes
2. UI Policies with that field in condition re-evaluate
3. Related onchange handlers fire
Form Submit Sequence:
1. onSubmit Client Scripts execute (by order)
2. If all return true, form submits
3. Server-side business rules fire
Procedure
Phase 1: Creating Client Scripts
Step 1.1: Create an onLoad Script
Basic onLoad Structure:
Tool: SN-Create-Record
Parameters:
table_name: sys_script_client
data:
name: "Initialize Request Form"
table: incident
type: onLoad
script: |
function onLoad() {
// Set default values
g_form.setValue('contact_type', 'email');
// Hide fields for new records
if (g_form.isNewRecord()) {
g_form.setDisplay('resolution_notes', false);
g_form.setDisplay('resolved_by', false);
}
// Show informational message
g_form.addInfoMessage('Please provide detailed information for faster resolution.');
}
active: true
order: 100
ui_type: 0
ui_type Values:
| Value | Meaning |
|---|---|
| 0 | Desktop |
| 1 | Mobile/Service Portal |
| 10 | Both Desktop and Mobile |
Step 1.2: Create an onChange Script
onChange with Field Dependency:
Tool: SN-Create-Record
Parameters:
table_name: sys_script_client
data:
name: "Category Sets Subcategory Options"
table: incident
type: onChange
field_name: category
script: |
function onChange(control, oldValue, newValue, isLoading, isTemplate) {
// Skip if loading form or using template
if (isLoading || isTemplate) {
return;
}
// Clear dependent field when parent changes
g_form.clearValue('subcategory');
// Set subcategory based on category
if (newValue == 'hardware') {
g_form.setValue('assignment_group', 'Hardware Support');
} else if (newValue == 'software') {
g_form.setValue('assignment_group', 'Software Support');
}
}
active: true
order: 100
ui_type: 0
onChange Parameters Explained:
| Parameter | Description |
|---|---|
| control | The form element (rarely used) |
| oldValue | Previous field value |
| newValue | New field value (current) |
| isLoading | true if form is loading |
| isTemplate | true if using a template |
Step 1.3: Create an onSubmit Script
Validation onSubmit:
Tool: SN-Create-Record
Parameters:
table_name: sys_script_client
data:
name: "Validate Required Fields Before Submit"
table: incident
type: onSubmit
script: |
function onSubmit() {
// Get field values
var shortDesc = g_form.getValue('short_description');
var category = g_form.getValue('category');
var priority = g_form.getValue('priority');
// Validate short description length
if (shortDesc.length < 10) {
g_form.addErrorMessage('Short description must be at least 10 characters.');
g_form.flash('short_description', '#FF0000', 0);
return false; // Prevent submission
}
// Validate P1 requires assignment group
if (priority == '1' && !g_form.getValue('assignment_group')) {
g_form.addErrorMessage('P1 incidents require an assignment group.');
return false;
}
// Confirm high priority submission
if (priority == '1' || priority == '2') {
var confirmed = confirm('You are submitting a high priority incident. Continue?');
if (!confirmed) {
return false;
}
}
return true; // Allow submission
}
active: true
order: 100
ui_type: 0
Step 1.4: Create an onCellEdit Script
List Editing Validation:
Tool: SN-Create-Record
Parameters:
table_name: sys_script_client
data:
name: "Validate Priority Change in List"
table: incident
type: onCellEdit
script: |
function onCellEdit(sysIDs, table, oldValues, newValue, callback) {
// sysIDs: array of selected record sys_ids
// oldValues: array of previous values
// newValue: the new value being set
// callback: must be called to complete the edit
var priority = g_form.getValue('priority');
// Prevent bulk P1 changes
if (newValue == '1' && sysIDs.length > 1) {
g_form.addErrorMessage('Cannot bulk change to P1. Edit records individually.');
callback(false); // Cancel edit
return;
}
// Confirm P1 assignment
if (newValue == '1') {
var confirmed = confirm('Setting priority to Critical. This will escalate the incident. Continue?');
callback(confirmed);
return;
}
callback(true); // Allow edit
}
active: true
order: 100
Phase 2: The g_form API
Step 2.1: Getting and Setting Values
Essential g_form Methods:
// Get field values
var value = g_form.getValue('field_name'); // Internal value
var display = g_form.getDisplayValue('field_name'); // Display value
var reference = g_form.getReference('assigned_to'); // Reference object (deprecated - use GlideAjax)
// Set field values
g_form.setValue('field_name', 'value'); // Set value
g_form.setValue('assigned_to', sysId, displayValue); // Set reference with display
g_form.clearValue('field_name'); // Clear to empty
// Check field states
var isEmpty = g_form.getValue('field_name') == ''; // Check empty
var isNewRecord = g_form.isNewRecord(); // New vs existing
Reference Field Handling:
// DEPRECATED - Makes synchronous server call (performance issue!)
var user = g_form.getReference('assigned_to');
var email = user.email;
// BETTER - Use callback (still not ideal)
g_form.getReference('assigned_to', function(ref) {
var email = ref.email;
// Continue processing
});
// BEST - Use GlideAjax (see Phase 4)
Step 2.2: Visibility and State Control
// Visibility
g_form.setDisplay('field_name', true); // Show field (affects row)
g_form.setVisible('field_name', true); // Show field (preserves space)
g_form.hideFieldMsg('field_name'); // Hide field message
// State control
g_form.setMandatory('field_name', true); // Make required
g_form.setReadOnly('field_name', true); // Make read-only
g_form.setDisabled('field_name', true); // Disable (grayed out)
// Labels
g_form.setLabelOf('field_name', 'New Label'); // Change label text
// Options (choice fields)
g_form.clearOptions('priority'); // Remove all options
g_form.addOption('priority', '1', 'Critical', 0); // Add option (value, label, index)
g_form.removeOption('priority', '5'); // Remove specific option
Step 2.3: Messages and Highlighting
// Form-level messages
g_form.addInfoMessage('Information message');
g_form.addWarningMessage('Warning message');
g_form.addErrorMessage('Error message');
g_form.clearMessages(); // Clear all messages
// Field-level messages
g_form.showFieldMsg('field_name', 'Message text', 'info'); // info, warning, error
g_form.hideFieldMsg('field_name'); // Clear field message
g_form.hideAllFieldMsgs(); // Clear all field messages
// Visual highlighting
g_form.flash('field_name', '#FF0000', 0); // Flash red (color, count; 0=once)
Step 2.4: Section and Related List Control
// Sections (tabs)
g_form.setSectionDisplay('section_name', true); // Show/hide section
g_form.isSectionVisible('section_name'); // Check visibility
g_form.activateTab('section_name'); // Switch to tab
// Related lists (limited support)
// Use UI Actions or GlideAjax for related list operations
Phase 3: g_user and g_scratchpad Objects
Step 3.1: The g_user Object
The g_user object provides information about the currently logged-in user:
// User identification
var userSysId = g_user.userID; // User sys_id
var userName = g_user.userName; // Username (login name)
var firstName = g_user.firstName; // First name
var lastName = g_user.lastName; // Last name
var fullName = g_user.getFullName(); // Full display name
// Role checks
var isAdmin = g_user.hasRole('admin'); // Check single role
var isItil = g_user.hasRoleExactly('itil'); // Exact role match
var hasAnyRole = g_user.hasRoles(); // Has any role
// Client data (set in business rules)
var customData = g_user.getClientData('custom_key');
// Preferences
var pref = g_user.getPreference('preference_name');
Setting Client Data from Server (Business Rule):
// Server-side (business rule, before query/display)
gs.getSession().putClientData('manager_email', current.caller_id.manager.email);
// Client-side (client script)
var managerEmail = g_user.getClientData('manager_email');
Step 3.2: The g_scratchpad Object
g_scratchpad passes data from server to client during form load:
Server-Side (Display Business Rule):
// Type: display, When: before
// Set scratchpad values for client access
g_scratchpad.isVip = current.caller_id.vip == true;
g_scratchpad.callerCompany = current.caller_id.company.name.toString();
g_scratchpad.maxPriority = gs.getProperty('incident.max_priority', '3');
Client-Side (onLoad Script):
function onLoad() {
// Access scratchpad data (no server call needed!)
if (g_scratchpad.isVip) {
g_form.addInfoMessage('VIP Caller - Handle with priority');
g_form.setValue('priority', '2');
}
// Use server-side property value
var maxPriority = g_scratchpad.maxPriority;
// Remove low priority options for VIP
if (g_scratchpad.isVip) {
g_form.removeOption('priority', '5');
g_form.removeOption('priority', '4');
}
}
Phase 4: GlideAjax for Server Calls
Step 4.1: Create a Script Include
First, create a client-callable Script Include:
Tool: SN-Create-Record
Parameters:
table_name: sys_script_include
data:
name: "IncidentAjaxUtils"
api_name: IncidentAjaxUtils
client_callable: true
script: |
var IncidentAjaxUtils = Class.create();
IncidentAjaxUtils.prototype = Object.extendsObject(AbstractAjaxProcessor, {
// Get user details by sys_id
getUserDetails: function() {
var userId = this.getParameter('sysparm_user_id');
var result = {};
var user = new GlideRecord('sys_user');
if (user.get(userId)) {
result.name = user.name.toString();
result.email = user.email.toString();
result.phone = user.phone.toString();
result.department = user.department.getDisplayValue();
result.manager = user.manager.getDisplayValue();
result.vip = user.vip == true;
}
return JSON.stringify(result);
},
// Validate assignment group can handle priority
validateAssignment: function() {
var groupId = this.getParameter('sysparm_group_id');
var priority = this.getParameter('sysparm_priority');
var result = { valid: true, message: '' };
var group = new GlideRecord('sys_user_group');
if (group.get(groupId)) {
// Check if group handles this priority
var canHandleP1 = group.u_handles_critical == true;
if (priority == '1' && !canHandleP1) {
result.valid = false;
result.message = group.name + ' does not handle Critical incidents. Please select a Critical-capable group.';
}
}
return JSON.stringify(result);
},
// Get related incidents count
getRelatedIncidentCount: function() {
var ciId = this.getParameter('sysparm_ci_id');
var count = 0;
if (ciId) {
var ga = new GlideAggregate('incident');
ga.addQuery('cmdb_ci', ciId);
ga.addQuery('active', true);
ga.addAggregate('COUNT');
ga.query();
if (ga.next()) {
count = ga.getAggregate('COUNT');
}
}
return count.toString();
},
type: 'IncidentAjaxUtils'
});
access: public
active: true
Step 4.2: Call GlideAjax from Client Script
Basic GlideAjax Pattern:
function onChange(control, oldValue, newValue, isLoading, isTemplate) {
if (isLoading || isTemplate || !newValue) {
return;
}
// Create GlideAjax call
var ga = new GlideAjax('IncidentAjaxUtils');
ga.addParam('sysparm_name', 'getUserDetails'); // Method name
ga.addParam('sysparm_user_id', newValue); // Custom parameter
// Make asynchronous call
ga.getXMLAnswer(function(response) {
// Parse JSON response
var user = JSON.parse(response);
if (user.name) {
// Update form with retrieved data
g_form.setValue('u_caller_email', user.email);
g_form.setValue('u_caller_phone', user.phone);
// VIP handling
if (user.vip) {
g_form.addInfoMessage('VIP Caller: ' + user.name);
g_form.setValue('priority', '2');
}
}
});
}
Validation with GlideAjax:
function onChange(control, oldValue, newValue, isLoading, isTemplate) {
if (isLoading || isTemplate || !newValue) {
return;
}
var priority = g_form.getValue('priority');
var ga = new GlideAjax('IncidentAjaxUtils');
ga.addParam('sysparm_name', 'validateAssignment');
ga.addParam('sysparm_group_id', newValue);
ga.addParam('sysparm_priority', priority);
ga.getXMLAnswer(function(response) {
var result = JSON.parse(response);
if (!result.valid) {
g_form.showFieldMsg('assignment_group', result.message, 'error');
g_form.setValue('assignment_group', '');
} else {
g_form.hideFieldMsg('assignment_group');
}
});
}
Step 4.3: GlideAjax with getXML (Full Response)
For more control over the response:
function onLoad() {
var ciId = g_form.getValue('cmdb_ci');
if (!ciId) return;
var ga = new GlideAjax('IncidentAjaxUtils');
ga.addParam('sysparm_name', 'getRelatedIncidentCount');
ga.addParam('sysparm_ci_id', ciId);
ga.getXML(function(response) {
// Get the answer element
var answer = response.responseXML.documentElement.getAttribute('answer');
var count = parseInt(answer) || 0;
if (count > 5) {
g_form.addWarningMessage('This CI has ' + count + ' active incidents. Consider checking for related problems.');
}
});
}
Phase 5: Performance Best Practices
Step 5.1: Minimize Server Calls
// BAD - Multiple synchronous calls
function onLoad() {
var caller = g_form.getReference('caller_id'); // Server call 1
var assignee = g_form.getReference('assigned_to'); // Server call 2
var group = g_form.getReference('assignment_group'); // Server call 3
// Form takes 3+ seconds to load!
}
// BETTER - Use g_scratchpad (set in Display Business Rule)
function onLoad() {
// Data already available - no server calls!
var callerVip = g_scratchpad.callerVip;
var assigneeEmail = g_scratchpad.assigneeEmail;
}
// BEST - Single GlideAjax call returning all needed data
function onLoad() {
var ga = new GlideAjax('FormDataUtils');
ga.addParam('sysparm_name', 'getFormContext');
ga.addParam('sysparm_incident_id', g_form.getUniqueValue());
ga.getXMLAnswer(function(response) {
var data = JSON.parse(response);
// All data in one call
processFormData(data);
});
}
Step 5.2: Use Conditions Wisely
// BAD - Script runs for every incident
function onLoad() {
if (g_form.getValue('priority') == '1') {
// Logic only needed for P1
}
}
// BETTER - Use script condition in client script record
// Condition: priority=1
function onLoad() {
// Only runs when priority is 1
// No condition check needed in script
}
Set Condition in Client Script Record:
Tool: SN-Create-Record
Parameters:
table_name: sys_script_client
data:
name: "P1 Special Handling"
table: incident
type: onLoad
condition: "priority=1"
script: |
function onLoad() {
// Only executes when condition matches
g_form.addInfoMessage('Critical incident - expedited handling required');
}
active: true
Step 5.3: Debounce Rapid Changes
// BAD - Ajax call on every keystroke
function onChange(control, oldValue, newValue, isLoading, isTemplate) {
if (isLoading) return;
// Fires on every character typed!
var ga = new GlideAjax('SearchUtils');
ga.addParam('sysparm_name', 'search');
ga.addParam('sysparm_query', newValue);
ga.getXMLAnswer(handleResults);
}
// BETTER - Debounce rapid changes
var searchTimeout;
function onChange(control, oldValue, newValue, isLoading, isTemplate) {
if (isLoading) return;
// Clear previous timeout
if (searchTimeout) {
clearTimeout(searchTimeout);
}
// Wait 500ms after last change before searching
searchTimeout = setTimeout(function() {
var ga = new GlideAjax('SearchUtils');
ga.addParam('sysparm_name', 'search');
ga.addParam('sysparm_query', newValue);
ga.getXMLAnswer(handleResults);
}, 500);
}
Step 5.4: Cache Reference Data
// Store lookup results to avoid repeated calls
var categoryAssignments = {};
function onChange(control, oldValue, newValue, isLoading, isTemplate) {
if (isLoading || !newValue) return;
// Check cache first
if (categoryAssignments[newValue]) {
g_form.setValue('assignment_group', categoryAssignments[newValue]);
return;
}
// Fetch and cache
var ga = new GlideAjax('CategoryUtils');
ga.addParam('sysparm_name', 'getAssignmentGroup');
ga.addParam('sysparm_category', newValue);
ga.getXMLAnswer(function(response) {
var groupId = response;
categoryAssignments[newValue] = groupId; // Cache for next time
g_form.setValue('assignment_group', groupId);
});
}
Phase 6: Mobile and Service Portal Considerations
Step 6.1: Mobile-Compatible Scripts
Tool: SN-Create-Record
Parameters:
table_name: sys_script_client
data:
name: "Mobile-Compatible Validation"
table: incident
type: onSubmit
ui_type: 10
script: |
function onSubmit() {
// These g_form methods work on mobile
var shortDesc = g_form.getValue('short_description');
if (!shortDesc || shortDesc.length < 5) {
// Use g_form messages (works on mobile)
g_form.addErrorMessage('Please provide a description');
return false;
}
// AVOID: alert(), confirm(), prompt() - don't work on mobile
// AVOID: document.getElementById() - DOM may differ
// AVOID: jQuery selectors - not available
return true;
}
active: true
Mobile-Compatible g_form Methods:
| Method | Desktop | Mobile | Service Portal |
|---|---|---|---|
| getValue() | Yes | Yes | Yes |
| setValue() | Yes | Yes | Yes |
| setMandatory() | Yes | Yes | Yes |
| setDisplay() | Yes | Yes | Yes |
| setReadOnly() | Yes | Yes | Yes |
| addErrorMessage() | Yes | Yes | Yes |
| showFieldMsg() | Yes | Yes | Yes |
| getReference() | Yes | No | Limited |
| flash() | Yes | No | No |
| activateTab() | Yes | No | No |
Step 6.2: Service Portal UI Scripts
For Service Portal-specific client logic, use UI Scripts:
Tool: SN-Create-Record
Parameters:
table_name: sys_ui_script
data:
name: "SP Incident Form Handler"
script: |
// Service Portal UI Script
// Runs in the Service Portal context
function spIncidentFormInit() {
// Use $scope for Angular integration
// Access widget data
console.log('SP Incident form initialized');
}
// Self-executing for immediate availability
if (typeof angular !== 'undefined') {
angular.element(document).ready(function() {
spIncidentFormInit();
});
}
active: true
global: false
ui_type: 10
Phase 7: Debugging Client Scripts
Step 7.1: Console Logging
function onLoad() {
// Development logging (remove in production)
console.log('=== Client Script: Initialize Form ===');
console.log('Record sys_id:', g_form.getUniqueValue());
console.log('Is new record:', g_form.isNewRecord());
console.log('Current user:', g_user.userName);
// Log scratchpad contents
console.log('Scratchpad:', JSON.stringify(g_scratchpad));
// Performance timing
console.time('formSetup');
// ... form setup logic ...
console.timeEnd('formSetup');
}
Step 7.2: JavaScript Debugger
Shortened here. Read the whole file on GitHub.
Signals
- GitHub stars
- 37
- Forks
- 13
- Last commit
- Jul 2026
Advanced
- Catalog kind
- skill
- Gateway key
client-scripts- Source
- github.com/happy-technologies-llc/happy-platform-skills