Client Scripts

SkillDev tools

Complete 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.

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_admin or admin
  • Access: sys_script_client, sys_ui_script tables
  • Knowledge: JavaScript fundamentals, ServiceNow form architecture
  • Related Skills: catalog/ui-policies for simpler show/hide/mandatory logic

Understanding Client Scripts

Script Type Comparison

TypeTriggerUse CasePerformance Impact
onLoadForm loadsInitialize fields, set defaultsMedium
onChangeField value changesField dependencies, cascading logicLow-Medium
onSubmitForm submissionValidation, confirmationLow
onCellEditList cell editedList editing validationLow

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:

ValueMeaning
0Desktop
1Mobile/Service Portal
10Both 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:

ParameterDescription
controlThe form element (rarely used)
oldValuePrevious field value
newValueNew field value (current)
isLoadingtrue if form is loading
isTemplatetrue 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:

MethodDesktopMobileService Portal
getValue()YesYesYes
setValue()YesYesYes
setMandatory()YesYesYes
setDisplay()YesYesYes
setReadOnly()YesYesYes
addErrorMessage()YesYesYes
showFieldMsg()YesYesYes
getReference()YesNoLimited
flash()YesNoNo
activateTab()YesNoNo
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