ServiceNow Client Scripts are JavaScript code snippets that run directly in the user’s browser, enabling dynamic form behavior, instant validation, and interactive user experiences—all without requiring a round trip to the server.
This guide walks you through the fundamentals, the four types of Client Scripts, and ten practical, real-world examples you can implement immediately.
What Are ServiceNow Client Scripts?
Client Scripts execute on the client-side (in the user’s browser) to dynamically control forms and fields. Unlike server-side Business Rules, they provide immediate feedback to users, resulting in a more responsive experience.
Key Characteristics
- Execution Environment: User’s browser
- Primary API: GlideForm (
g_form) - Purpose: Form manipulation, field validation, user interaction
- Performance Impact: Minimal server load
- User Experience: Instantaneous feedback without page reloads
When to Use Client Scripts
- Validating form data before submission
- Showing or hiding fields based on selections
- Setting dynamic default values
- Providing real-time feedback
- Controlling mandatory field requirements
The 4 Types of Client Scripts
1. onLoad Scripts
Runs: When a form loads and fields are populated.
Best for:
- Setting default values
- Hiding/showing fields based on initial conditions
- Setting fields to read-only
- Displaying informational messages
Performance Note: Keep onLoad scripts lightweight—they impact form load time.
2. onChange Scripts
Runs: When the value of a specified field changes.
Best for:
- Cascading field updates
- Dynamic field validations
- Conditional field visibility
- Auto-populating related fields
Important: Only applies to the specific field defined in the script.
3. onSubmit Scripts
Runs: When a user submits a form, before data reaches the server.
Best for:
- Final form validations
- Confirming user actions
- Preventing submission based on conditions
- Displaying warning messages
Return Value: Must return
trueto allow submission orfalseto prevent it.
4. onCellEdit Scripts
Runs: When a user changes a field value in a list editor.
Best for:
- List view validations
- Updating related list fields
- Immediate feedback in list views
10 Real-Time Examples
Example 1: Auto-Populate Description Based on Category (onChange)
Use Case: When a user selects a category on an Incident form, automatically populate the description field with a template.
Type: onChange | Table: Incident [incident] | Field: Category
function onChange(control, oldValue, newValue, isLoading, isTemplate) {
if (isLoading || newValue == '') {
return;
}
var category = g_form.getValue('category');
var templates = {
'hardware': 'Hardware Issue:\n\nDevice Type:\nSerial Number:\nIssue Description:\nSteps to Reproduce:',
'software': 'Software Issue:\n\nApplication Name:\nVersion:\nError Message:\nSteps to Reproduce:',
'network': 'Network Issue:\n\nLocation:\nDevices Affected:\nIssue Description:\nStart Time:'
};
if (templates[category]) {
g_form.setValue('description', templates[category]);
}
}
Key Takeaways:
- Always check
isLoadingto prevent execution during form load - Validate that
newValueexists before processing - Use clear, structured templates
Example 2: Mandatory Field Based on Condition (onLoad & onChange)
Use Case: Make the “Justification” field mandatory when Priority is “Critical” or “High”.
Type: onChange | Table: Change Request [change_request] | Field: Priority
function onChange(control, oldValue, newValue, isLoading, isTemplate) {
if (isLoading || newValue == "") {
return;
}
var priority = g_form.getValue('priority');
if (priority == '1' || priority == '2') {
g_form.setMandatory('justification', true);
g_form.showFieldMsg('justification', 'Justification is required for high-priority changes', 'info');
} else {
g_form.setMandatory('justification', false);
g_form.hideFieldMsg('justification');
}
}
Companion onLoad Script:
function onLoad() {
var priority = g_form.getValue('priority');
if (priority == '1' || priority == '2') {
g_form.setMandatory('justification', true);
}
}
Key Takeaways:
- Use both onLoad and onChange for consistency
- Provide feedback with
showFieldMsg() - Priority values are stored as numbers
Example 3: Prevent Form Submission with Validation (onSubmit)
Use Case: Prevent submitting an Incident without a Configuration Item when category is “Hardware”.
Type: onSubmit | Table: Incident [incident]
function onSubmit() {
var category = g_form.getValue('category');
var configItem = g_form.getValue('cmdb_ci');
if (category == 'hardware' && configItem == '') {
g_form.addErrorMessage('Please select a Configuration Item for hardware-related incidents.');
g_form.flash('cmdb_ci', '#FF0000', 0);
return false;
}
return true;
}
Key Takeaways:
- Return
falseto prevent submission,trueto allow it - Use
addErrorMessage()for clear communication - Use
flash()to draw attention to problematic fields
Example 4: Hide/Show Fields Based on Selection (onChange)
Use Case: Show additional fields only when “Other” is selected in a dropdown.
Type: onChange | Table: Incident [incident] | Field: Close Code
function onChange(control, oldValue, newValue, isLoading, isTemplate) {
if (isLoading || newValue == "") {
return;
}
var closeCode = g_form.getValue('close_code');
if (closeCode == 'other') {
g_form.setVisible('close_notes', true);
g_form.setMandatory('close_notes', true);
g_form.showFieldMsg('close_notes', 'Please provide detailed closure notes', 'info');
} else {
g_form.setVisible('close_notes', false);
g_form.setMandatory('close_notes', false);
g_form.setValue('close_notes', '');
}
}
Key Takeaways:
- Clear hidden field values to prevent data confusion
- Remove mandatory status when hiding fields
- Always explain why fields appear
Example 5: Copy Values Between Fields (onChange)
Use Case: When “Use Caller’s Location” is checked, copy the caller’s location to the incident location.
Type: onChange | Table: Incident [incident]
function onChange(control, oldValue, newValue, isLoading) {
if (isLoading || newValue == 'false') {
return;
}
var callerSysId = g_form.getValue('caller_id');
if (callerSysId) {
var ga = new GlideAjax('LocationUtilsAjax');
ga.addParam('sysparm_name', 'getCallerLocation');
ga.addParam('sysparm_caller_id', callerSysId);
ga.getXMLAnswer(function(answer) {
if (answer) {
g_form.setValue('location', answer);
}
});
}
}
Alternative Direct Copy:
function onChange(control, oldValue, newValue, isLoading) {
if (isLoading || newValue == 'false') {
return;
}
var callerLocation = g_form.getReference('caller_id').location;
if (callerLocation) {
g_form.setValue('location', callerLocation);
}
}
Key Takeaways:
- Use GlideAjax for complex server-side data retrieval
- Use
getReference()for simple reference field access - Always check if source values exist
Example 6: Real-Time Field Validation (onChange)
Use Case: Validate email format as the user types.
Type: onChange | Table: User [sys_user] | Field: Email
function onChange(control, oldValue, newValue, isLoading) {
if (isLoading || newValue == '') {
return;
}
var emailPattern = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/;
if (!emailPattern.test(newValue)) {
g_form.showFieldMsg('email', 'Please enter a valid email address', 'error');
g_form.flash('email', '#FF0000', 0);
} else {
g_form.hideFieldMsg('email');
g_form.showFieldMsg('email', 'Email format is valid', 'success');
setTimeout(function() {
g_form.hideFieldMsg('email');
}, 3000);
}
}
Key Takeaways:
- Use regular expressions for pattern validation
- Provide both error and success feedback
- Consider timed message removal for UX
Example 7: Calculate and Display Total Cost (onChange)
Use Case: Calculate total cost from quantity and unit price on a Purchase Order.
Type: onChange | Table: Purchase Order Line [sc_req_item]
function onChange(control, oldValue, newValue, isLoading) {
if (isLoading) {
return;
}
calculateTotal();
}
function calculateTotal() {
var quantity = parseFloat(g_form.getValue('quantity')) || 0;
var unitPrice = parseFloat(g_form.getValue('price')) || 0;
var total = quantity * unitPrice;
g_form.setValue('total_cost', total.toFixed(2));
if (quantity > 0 && unitPrice > 0) {
g_form.showFieldMsg('total_cost',
quantity + ' × $' + unitPrice.toFixed(2) + ' = $' + total.toFixed(2),
'info');
}
}
Key Takeaways:
- Use
parseFloat()for numeric calculations - Provide defaults with
|| 0to prevent NaN errors - Use
toFixed(2)for currency formatting - Create reusable functions for shared logic
Example 8: Confirmation Dialog for Critical Actions (onSubmit)
Use Case: Confirm before closing a critical incident.
Type: onSubmit | Table: Incident [incident]
function onSubmit() {
var state = g_form.getValue('state');
var priority = g_form.getValue('priority');
var resolution = g_form.getValue('close_notes');
if (state == '7' && priority == '1') {
if (!resolution || resolution.length < 50) {
g_form.addErrorMessage('Critical incidents require detailed resolution notes (minimum 50 characters).');
return false;
}
var confirmMsg = 'You are closing a Critical incident. Please confirm:\n\n' +
'• Root cause has been identified\n' +
'• Proper resolution has been documented\n' +
'• User has confirmed resolution\n\n' +
'Proceed with closing this incident?';
if (!confirm(confirmMsg)) {
return false;
}
}
return true;
}
Key Takeaways:
- Use
confirm()for important user confirmations - Validate completeness of critical fields
- Provide clear checklist items
Example 9: Dynamic Field Options via GlideAjax (onChange)
Use Case: Filter available assignment groups based on the selected category.
Type: onChange | Table: Incident [incident] | Field: Category
function onChange(control, oldValue, newValue, isLoading, isTemplate) {
if (isLoading || newValue == '') {
return;
}
g_form.clearValue('assignment_group');
var ga = new GlideAjax('IncidentUtils');
ga.addParam('sysparm_name', 'getAssignmentGroups');
ga.addParam('sysparm_category', newValue);
ga.getXMLAnswer(function(answer) {
if (answer) {
var groups = JSON.parse(answer);
if (groups.length == 1) {
g_form.setValue('assignment_group', groups[0].sys_id);
g_form.showFieldMsg('assignment_group',
'Auto-assigned to: ' + groups[0].name, 'info');
} else if (groups.length > 1) {
g_form.showFieldMsg('assignment_group',
groups.length + ' assignment groups available for this category', 'info');
} else {
g_form.showFieldMsg('assignment_group',
'No specific assignment group found for this category', 'warning');
}
}
});
}
Required Script Include (IncidentUtils):
var IncidentUtils = Class.create();
IncidentUtils.prototype = Object.extendsObject(AbstractAjaxProcessor, {
getAssignmentGroups: function() {
var category = this.getParameter('sysparm_category');
var groups = [];
var gr = new GlideRecord('sys_user_group');
gr.addQuery('active', true);
gr.addQuery('u_supported_category', category);
gr.query();
while (gr.next()) {
groups.push({
sys_id: gr.getUniqueValue(),
name: gr.getValue('name')
});
}
return JSON.stringify(groups);
},
type: 'IncidentUtils'
});
Key Takeaways:
- GlideAjax enables server-side data retrieval
- Always parse JSON responses properly
- Clear dependent fields when parent changes
Example 10: Set Read-Only Fields Based on User Role (onLoad)
Use Case: Lock certain fields for users without specific roles.
Type: onLoad | Table: Change Request [change_request]
function onLoad() {
var hasChangeManagerRole = g_user.hasRole('change_manager');
var hasAdminRole = g_user.hasRole('admin');
if (!hasChangeManagerRole && !hasAdminRole) {
g_form.setReadOnly('approval', true);
g_form.setReadOnly('risk', true);
g_form.setReadOnly('impact', true);
g_form.addInfoMessage('Risk and approval fields are managed by Change Managers only.');
g_form.setSectionDisplay('approval_section', false);
}
var state = g_form.getValue('state');
if (state == 'implement' || state == 'closed') {
var fieldsToLock = ['short_description', 'description', 'start_date', 'end_date'];
fieldsToLock.forEach(function(field) {
g_form.setReadOnly(field, true);
});
g_form.addInfoMessage('This change is in implementation phase. Most fields are locked.');
}
}
Key Takeaways:
- Use
g_user.hasRole()for role-based logic - Combine conditions for complex scenarios
- Inform users why fields are read-only
Best Practices for Client Scripts
1. Always Check the isLoading Parameter
function onChange(control, oldValue, newValue, isLoading) {
if (isLoading) {
return;
}
// Your code here
}
Prevents unnecessary execution and potential infinite loops.
2. Minimize GlideAjax Calls
- Cache results when possible
- Combine multiple data requests into single calls
- Use asynchronous callbacks properly
Best Practice:
var ga = new GlideAjax('Utils');
ga.addParam('sysparm_name', 'getMultipleValues');
ga.getXMLAnswer(function(answer) {
var data = JSON.parse(answer);
// Process all data at once
});
3. Use Descriptive Naming and Comments
function onChange(control, oldValue, newValue, isLoading) {
if (isLoading) {
return;
}
var priorityValue = g_form.getValue('priority');
if (priorityValue == '1') {
enforceImmediateAssignment();
}
}
function enforceImmediateAssignment() {
g_form.setMandatory('assigned_to', true);
g_form.showFieldMsg('assigned_to',
'Critical priority requires immediate assignment', 'error');
}
4. Validate Input and Handle Edge Cases
function onChange(control, oldValue, newValue, isLoading) {
if (isLoading || newValue == '' || newValue == oldValue) {
return;
}
var quantity = parseInt(newValue);
if (isNaN(quantity) || quantity < 0) {
g_form.showFieldMsg('quantity', 'Please enter a valid positive number', 'error');
g_form.setValue('quantity', oldValue);
return;
}
}
5. Avoid Large Data in g_scratchpad
Use g_scratchpad only for small, essential data—it increases page size.
6. Test with Different User Roles and Scenarios
Test as different user types, with various data combinations, on mobile and desktop.
7. Use UI Policies When Possible
UI Policy for: Simple show/hide, basic mandatory rules, read-only settings, simple field clearing.
Client Scripts for: Complex calculations, multi-field validations, AJAX calls, custom messages, advanced conditional logic.
8. Optimize Performance
// Better: Store in variable
var priority = g_form.getValue('priority');
if (priority == '1' || priority == '2') {
// Logic
}
9. Handle Errors Gracefully
function onChange(control, oldValue, newValue, isLoading) {
try {
if (isLoading) return;
var result = performCalculation(newValue);
g_form.setValue('result', result);
} catch (error) {
g_form.addErrorMessage('An error occurred. Please contact support.');
console.error('Client Script Error:', error);
}
}
10. Document Your Scripts
/*
* Client Script: Auto-populate Assignment Group
* Type: onChange
* Table: Incident
* Field: Category
*
* Description: When category changes, automatically suggests appropriate
* assignment group based on category-to-group mapping table.
*
* Author: Your Name
* Date: 2024-01-15
*/
Common Troubleshooting Tips
Issue 1: Script Not Executing
Possible Causes:
- Script is inactive → Enable the “Active” checkbox
- Wrong table → Verify the “Table” field matches your form
- Conditions not met → Test without conditions first
- UI Policy conflict → Adjust execution order
Debug with:
console.log('Script executing');
console.log('Old Value:', oldValue);
console.log('New Value:', newValue);
console.log('Is Loading:', isLoading);
Issue 2: Infinite Loop or Performance Problems
Causes:
- Missing
isLoadingcheck - Circular onChange scripts (Field A sets B, B sets A)
- Too many GlideAjax calls (add debouncing)
Issue 3: GlideAjax Not Working
Checklist:
- ✓ “Client callable” checkbox is selected
- ✓ Script Include extends
AbstractAjaxProcessor - ✓ Function name matches
sysparm_nameparameter - ✓ Callback function is properly defined
Issue 4: Fields Not Updating
- Verify field names are correct (use browser inspect)
- Reference fields require sys_id, not display value
- Timing issues may require
setTimeout()
Issue 5: Works on Desktop, Not Mobile
- Avoid
confirm(),alert(),prompt() - Use
g_form.addInfoMessage()andg_form.showFieldMsg() - Detect mobile with
g_form.isMobile()
Issue 6: Mandatory Field Not Preventing Submission
// WRONG - Missing return
function onSubmit() {
if (validateForm()) {
false; // Does nothing!
}
}
// CORRECT
function onSubmit() {
if (!validateForm()) {
return false;
}
return true;
}
Pro Tip: Enable Client Script Debugging
- Navigate to System Properties > UI Properties
- Search for
glide.ui.security.allow_client_script_debugging - Set to
true - Add
&sysparm_client_script_debug=trueto the URL
Frequently Asked Questions
What is the difference between Client Scripts and Business Rules?
Client Scripts run on the client-side (browser) for form interactions and immediate feedback. Business Rules run server-side for data validation, complex logic, and database operations.
Can I use server-side APIs in Client Scripts?
No. Use GlideAjax to call Script Includes that execute server-side code.
How many Client Scripts can I have on a single form?
There’s no hard limit, but too many impact performance. Keep under 10 per form and consolidate logic.
Will Client Scripts work on Service Portal?
No. Use Catalog Client Scripts or Widget Controllers instead.
How do I debug Client Scripts?
Use browser Developer Tools (F12), check the Console tab, add console.log() statements, and enable ServiceNow’s client script debugging.
Conclusion
Mastering ServiceNow Client Scripts is essential for building efficient, user-friendly applications. Key takeaways:
- Choose the Right Type: onLoad, onChange, onSubmit, or onCellEdit
- Always Validate: Check
isLoading, validate inputs, handle edge cases - Optimize Performance: Minimize AJAX calls, cache values
- Provide User Feedback: Use informative messages and visual cues
- Test Thoroughly: Verify across roles, scenarios, and devices
Start small, test in sub-production, document everything, and monitor performance. Happy scripting!
