What It Is

Client Scripts are JavaScript functions that execute in the user's browser to control form behavior, validate input, and manipulate the user interface in real-time. They solve the fundamental problem of providing immediate feedback and dynamic form behavior without round trips to the server. While Business Rules and other server-side scripts handle data integrity and workflow logic after submission, Client Scripts operate at the presentation layer where users actually interact with your forms.

Architecturally, Client Scripts sit at the browser boundary of ServiceNow's client-server architecture. When a user loads a form, ServiceNow embeds your Client Script code directly into the rendered HTML page, where it executes within the browser's JavaScript engine. This client-side execution gives you access to the DOM and browser APIs, but isolates you from server-side resources like GlideRecord queries or direct database access. You communicate back to the server through GlideAjax calls to Script Includes, creating a clear separation between presentation logic and business logic.

ServiceNow processes Client Scripts by injecting them into form pages at render time through its form generation engine. The platform automatically wraps your code in the appropriate event handlers (onLoad, onChange, onSubmit, onCellEdit) and provides the g_form object as your primary interface to the form. Each script runs in a shared global scope with other Client Scripts on the same form, which creates both opportunities for code reuse and dangerous potential for variable collisions and unintended interactions between scripts.

Without Client Scripts, you cannot provide instant field validation, dynamic field visibility, conditional mandatory fields, or any form behavior that responds immediately to user input. Server-side scripts like Business Rules only execute on save, meaning users would have to submit invalid forms to receive feedback. Client Scripts are essential for modern user experience expectations: hiding irrelevant fields based on category selection, calculating totals as users type, preventing form submission with invalid data, or showing helpful information tooltips. Any form interaction that feels responsive and immediate requires Client Script logic.

Administrators typically use simple onChange Client Scripts for basic field manipulations like setting field values or mandatory states based on business rules. Developers build complex Client Scripts that integrate with Script Includes for server-side data lookups, implement sophisticated validation logic, and create dynamic user interfaces that adapt to user selections. Architects use Client Scripts as part of broader application design, often establishing patterns for how client-side and server-side code should interact, defining reusable utility functions, and ensuring performance across large implementations with hundreds of forms.

Client Scripts work hand-in-hand with Script Includes for server-side data access, UI Policies for declarative field behavior, and Business Rules for server-side validation. While UI Policies can handle simple show/hide and mandatory logic without code, Client Scripts provide the flexibility to implement complex conditional logic that UI Policies cannot express. Client Scripts often call Script Includes via GlideAjax to perform server-side operations like querying related records or validating data against business rules, while Business Rules serve as the final server-side validation layer that Client Scripts cannot bypass.

How It Works Under the Hood

When ServiceNow renders a form, it queries the sys_script_client table for all active Client Scripts that match the current table and form context. The platform embeds these scripts directly into the generated HTML page as JavaScript functions, wrapped in the appropriate event handlers. Each script type gets registered with ServiceNow's form framework: onLoad scripts execute after form rendering completes, onChange scripts bind to specific field change events, and onSubmit scripts intercept form submissions before they reach the server.

The g_form object serves as your primary API interface, providing methods to manipulate fields, sections, and form behavior. Behind the scenes, g_form methods often trigger additional ServiceNow framework code that updates the DOM, manages field dependencies, and maintains form state. When you call g_form.setValue(), ServiceNow not only updates the field value but also triggers any UI Policies, dependent field calculations, and other onChange Client Scripts that depend on that field. This cascading behavior means a single Client Script action can trigger a complex chain of form updates that developers don't always anticipate.

Client Scripts execute in the browser's JavaScript engine with access to standard DOM APIs, but ServiceNow provides additional global objects like g_user for user information, g_scratchpad for server-to-client data transfer, and GlideAjax for server communication. The platform also injects form-specific data like field choice lists, user permissions, and table metadata, making this information available to your scripts without additional server requests. All Client Scripts on a form share the same global namespace, which means variable declarations without proper scoping can create conflicts between different scripts.

The Execution Lifecycle

  1. User requests a form (new record, edit existing, or form view) and ServiceNow's form engine queries active Client Scripts for the target table
  2. Platform embeds Client Script code into the HTML page as JavaScript functions, registering each script with the appropriate event handler based on type
  3. Browser renders the form HTML and ServiceNow's client-side framework initializes, creating the g_form object and other global APIs
  4. All onLoad Client Scripts execute in order of creation (based on sys_created_on), performing initial form setup, field visibility, and validation rules
  5. As users interact with fields, corresponding onChange scripts execute, potentially triggering cascading field updates, UI Policy evaluations, and dependent script execution
  6. When user submits the form, all onSubmit scripts execute and can prevent submission by returning false, before data reaches server-side Business Rules and other processing
Free Newsletter

Enjoying this? Get one deep-dive per week.

Join 1,000+ ServiceNow pros — scripts, GlideRecord patterns, Flow Designer techniques, and career moves. Free.

No spam · Unsubscribe anytime

The Core Pattern

Client Script — Incident Priority Calculator.js
function onChange(control, oldValue, newValue, isLoading, isTemplate) {
    // Exit early during form load to avoid unnecessary processing
    if (isLoading || newValue === '') return;
    
    // Only process when impact or urgency changes
    var fieldName = control.toString();
    if (fieldName !== 'impact' && fieldName !== 'urgency') return;
    
    var impact = g_form.getValue('impact');
    var urgency = g_form.getValue('urgency');
    
    // Both fields must have values to calculate priority
    if (impact && urgency) {
        // Call server-side Script Include for business logic
        var ga = new GlideAjax('IncidentPriorityUtils');
        ga.addParam('sysparm_name', 'calculatePriority');
        ga.addParam('impact', impact);
        ga.addParam('urgency', urgency);
        ga.getXMLAnswer(function(answer) {
            if (answer) {
                g_form.setValue('priority', answer);
            }
        });
    }
}
Script Include — IncidentPriorityUtils.js
var IncidentPriorityUtils = Class.create();
IncidentPriorityUtils.prototype = Object.extendsObject(AbstractAjaxProcessor, {
    
    calculatePriority: function() {
        var impact = this.getParameter('impact');
        var urgency = this.getParameter('urgency');
        
        // Validate inputs exist and are numeric
        if (!impact || !urgency || isNaN(impact) || isNaN(urgency)) {
            return '';
        }
        
        // Business logic: lower numbers = higher priority
        // Priority matrix: I1+U1=P1, I1+U2=P2, I2+U1=P2, etc.
        var priority = Math.max(parseInt(impact), parseInt(urgency));
        
        // Cap at priority 4 (lowest priority)
        return Math.min(priority, 4).toString();
    },
    
    type: 'IncidentPriorityUtils'
});

Real-World Scenarios

Service Catalog Item Dynamic Pricing

A laptop ordering form needs to calculate total cost based on selected options like RAM, storage, and software packages. The pricing calculation requires server-side access to current pricing data and discount rules based on user's department.

Client Script — Laptop Request Pricing.js
function onChange(control, oldValue, newValue, isLoading) {
    if (isLoading) return;
    
    // Trigger pricing calculation for any option field change
    var priceFields = ['ram_upgrade', 'storage_upgrade', 'software_package'];
    if (priceFields.indexOf(control.toString()) === -1) return;
    
    // Show loading indicator
    g_form.addInfoMessage('Calculating price...');
    
    var ga = new GlideAjax('CatalogPricingUtil');
    ga.addParam('sysparm_name', 'calculateLaptopPrice');
    ga.addParam('ram_upgrade', g_form.getValue('ram_upgrade'));
    ga.addParam('storage_upgrade', g_form.getValue('storage_upgrade'));
    ga.addParam('software_package', g_form.getValue('software_package'));
    ga.addParam('user_department', g_user.department);
    
    ga.getXMLAnswer(function(response) {
        g_form.clearMessages();
        if (response) {
            var pricing = JSON.parse(response);
            g_form.setValue('estimated_cost', pricing.total);
            g_form.setValue('discount_applied', pricing.discount);
        }
    });
}
Script Include — CatalogPricingUtil.js
calculateLaptopPrice: function() {
    var basePrice = 1200; // Base laptop cost
    var total = basePrice;
    var discount = 0;
    
    // Add upgrade costs from pricing table
    var ramUpgrade = this.getParameter('ram_upgrade');
    var storageUpgrade = this.getParameter('storage_upgrade');
    var softwarePackage = this.getParameter('software_package');
    
    if (ramUpgrade) total += this._getUpgradePrice('ram', ramUpgrade);
    if (storageUpgrade) total += this._getUpgradePrice('storage', storageUpgrade);
    if (softwarePackage) total += this._getUpgradePrice('software', softwarePackage);
    
    // Apply department discount
    var department = this.getParameter('user_department');
    if (department === 'IT' || department === 'Engineering') {
        discount = total * 0.15; // 15% discount for technical departments
        total = total - discount;
    }
    
    return JSON.stringify({total: total.toFixed(2), discount: discount.toFixed(2)});
}

Watch for performance issues when multiple option fields change rapidly - consider debouncing the Ajax calls to avoid overwhelming the server. Also ensure your pricing calculation handles edge cases like deleted catalog options or users without department assignments.

Change Request Risk Assessment Validation

Change requests require risk assessment validation before submission, checking that all required risk fields are completed based on the change type and impact. The validation must prevent submission and provide clear guidance on missing requirements.

Client Script — Change Request Validation.js
function onSubmit() {
    var changeType = g_form.getValue('type');
    var impact = g_form.getValue('impact');
    var errors = [];
    
    // High risk changes require additional documentation
    if (changeType === 'emergency' || impact === '1') {
        if (!g_form.getValue('risk_assessment')) {
            errors.push('Risk Assessment is required for high-impact changes');
        }
        if (!g_form.getValue('backout_plan')) {
            errors.push('Backout Plan is required for high-impact changes');
        }
        if (!g_form.getValue('test_plan')) {
            errors.push('Test Plan is required for high-impact changes');
        }
    }
    
    // Standard changes need approval justification
    if (changeType === 'standard' && !g_form.getValue('justification')) {
        errors.push('Justification is required for standard changes');
    }
    
    // Show all errors and prevent submission
    if (errors.length > 0) {
        g_form.addErrorMessage('Please complete the following:\n• ' + errors.join('\n• '));
        return false;
    }
    
    return true;
}

Remember that onSubmit validation can be bypassed by users who disable JavaScript or submit forms programmatically. Always implement matching server-side validation in Business Rules as your final defense. Consider using g_form.addErrorMessage() instead of alert() for better user experience and mobile compatibility.

Asset Management Location Cascading

Asset forms need cascading location fields where selecting a building populates available floors, and selecting a floor shows only relevant rooms. This requires dynamic choice list updates based on hierarchical location data stored in a custom location table.

Client Script — Asset Location Cascade.js
function onChange(control, oldValue, newValue, isLoading) {
    if (isLoading) return;
    
    var fieldName = control.toString();
    
    if (fieldName === 'building') {
        // Clear dependent fields when building changes
        g_form.clearValue('floor');
        g_form.clearValue('room');
        
        if (newValue) {
            // Populate floor choices for selected building
            g_form.addOption('floor', '', '-- Select Floor --');
            var ga = new GlideAjax('LocationUtils');
            ga.addParam('sysparm_name', 'getFloorsByBuilding');
            ga.addParam('building_id', newValue);
            ga.getXMLAnswer(function(response) {
                if (response) {
                    var floors = JSON.parse(response);
                    g_form.clearOptions('floor');
                    g_form.addOption('floor', '', '-- Select Floor --');
                    for (var i = 0; i < floors.length; i++) {
                        g_form.addOption('floor', floors[i].sys_id, floors[i].name);
                    }
                }
            });
        } else {
            g_form.clearOptions('floor');
            g_form.clearOptions('room');
        }
    }
    
    if (fieldName === 'floor' && newValue) {
        // Similar logic for room population
        g_form.clearValue('room');
        // Ajax call to populate rooms...
    }
}
Script Include — LocationUtils.js
getFloorsByBuilding: function() {
    var buildingId = this.getParameter('building_id');
    var floors = [];
    
    if (!buildingId) return JSON.stringify(floors);
    
    // Query location hierarchy table
    var gr = new GlideRecord('u_location_hierarchy');
    gr.addQuery('parent', buildingId);
    gr.addQuery('type', 'floor');
    gr.addQuery('active', true);
    gr.orderBy('name');
    gr.query();
    
    while (gr.next()) {
        floors.push({
            sys_id: gr.getUniqueValue(),
            name: gr.getDisplayValue('name')
        });
    }
    
    return JSON.stringify(floors);
}

Cascading choice lists can create performance bottlenecks if not implemented carefully - consider caching location data in g_scratchpad for forms that load with existing values. Be careful with clearOptions() timing - clearing options before adding new ones can cause brief UI flicker and confusion for users.

The Classic Mistake

⚠️

Making synchronous GlideAjax calls from client scripts blocks the browser thread and creates timeout errors.

Anti-pattern — Do Not Use This.js
function onChange(control, oldValue, newValue, isLoading) {
    if (isLoading || newValue == '') return;
    
    var ga = new GlideAjax('MyScriptInclude');
    ga.addParam('sysparm_name', 'validateUser');
    ga.addParam('sysparm_user_id', newValue);
    
    // This kills the browser thread
    ga.getXMLWait();
    
    var result = ga.getAnswer();
    if (result == 'invalid') {
        alert('Invalid user selected');
        g_form.setValue('assigned_to', '');
    }
}

The getXMLWait() method blocks the entire browser thread until the server responds, which can take 3-30 seconds depending on server load. Users see a frozen interface with spinning cursors, and browser dev tools show "Script is taking too long" warnings. ServiceNow's transaction timeout can kill the request mid-flight, leaving the client script waiting indefinitely. The browser's JavaScript engine literally stops processing all other events, including clicks and form interactions.

The Fix.js
function onChange(control, oldValue, newValue, isLoading) {
    if (isLoading || newValue == '') return;
    
    var ga = new GlideAjax('MyScriptInclude');
    ga.addParam('sysparm_name', 'validateUser');
    ga.addParam('sysparm_user_id', newValue);
    
    // Async call with callback
    ga.getXML(function(response) {
        var result = response.responseXML.documentElement.getAttribute('answer');
        if (result == 'invalid') {
            alert('Invalid user selected');
            g_form.setValue('assigned_to', '');
        }
    });
}
💡

Never use getXMLWait() in client scripts. Always use getXML() with a callback function to keep the browser responsive.

Performance Rules

  1. Limit GlideAjax calls to maximum 3 per form load and 1 per field change. More than this creates network congestion and users report "slow forms" to system administrators within 48 hours of deployment.
  2. Never call g_form.setValue() inside loops that process more than 50 iterations. Each call triggers onChange scripts and DOM updates, causing browser memory usage to spike above 500MB and crash mobile browsers.
  3. Cache g_form.getValue() results in variables when calling the same field more than twice in a single script. DOM queries are expensive and stack up during rapid user input, creating 200ms+ response delays.
  4. Use debouncing with setTimeout() for onChange scripts that trigger on text fields. Without it, each keystroke fires the script and users typing fast generate 50+ server calls in 10 seconds.
  5. Avoid g_form.addOption() and g_form.clearOptions() for choice lists with more than 200 options. The browser hangs for 5+ seconds rebuilding the dropdown, and Internet Explorer crashes entirely.
  6. Never put try-catch blocks around entire client scripts unless logging errors. JavaScript engines optimize better without exception handling, and wrapped scripts run 40% slower in Chrome and Safari.
  7. Limit onSubmit script execution to under 2 seconds total. Longer scripts block form submission, and users click Submit multiple times, creating duplicate records in tables like sc_req_item and incident.

Side Effects & Platform Behavior

  • Client Scripts do NOT trigger Business Rules, Workflows, or Notifications when called via g_form.setValue() - only onChange Client Scripts fire on the same form
  • Field changes made in Client Scripts appear immediately in the sys_audit table when the form is submitted, with the current user as the modifier even if the script ran automatically
  • Scripts with Inherited = true run on all child tables but can break when child tables have different field schemas or UI policies
  • JavaScript errors in Client Scripts appear in the browser console only - they do NOT write to ServiceNow's System Log or generate notifications to administrators
  • onSubmit scripts that return false prevent form submission completely but still trigger other onSubmit scripts - use with g_form.addErrorMessage() to explain why
  • GlideAjax calls from Client Scripts create entries in the sys_user_session table and count against the concurrent user license limit
  • Client Scripts run BEFORE UI Policies on form load but AFTER UI Policies on field changes, creating timing conflicts with field visibility and mandatory settings
  • Scripts break completely in Service Portal unless wrapped in if (typeof window != 'undefined') checks because Portal uses server-side rendering
  • Mobile applications cache Client Scripts aggressively - users must clear app data to see script updates, unlike web browsers that refresh automatically

Debugging When It Breaks

Most Client Script failures show as silent breaks - forms don't respond to clicks, fields don't populate, or validations don't fire. Users report "the form is broken" without specific error details. Developers see form behavior that worked yesterday suddenly stop working, often after unrelated changes to UI Policies or Business Rules. The most frustrating symptom is intermittent failures where scripts work for some users but not others, typically caused by role-based data access differences or browser-specific JavaScript engine behaviors.

Start debugging by opening the browser's Developer Tools Console (F12) and refreshing the form - JavaScript errors appear immediately with stack traces pointing to specific line numbers. In ServiceNow, check System Definition > Client Scripts to verify the script is Active and has the correct Table setting. For GlideAjax issues, enable Debug logging in System Diagnostics > Debug and look for "AJAXProcessor" entries that show request/response timing and content. Mobile app debugging requires connecting the device to a desktop browser and using remote debugging tools.

Common error messages include "g_form is not defined" (script running before form initialization), "Cannot read property 'getValue' of undefined" (field doesn't exist on the form), "XMLHttpRequest failed" (GlideAjax timeout or Script Include doesn't exist), and "Script error. line: 0" (cross-origin or Content Security Policy violation). Quick diagnostic checklist:

  • Verify script Type matches your use case (onLoad, onChange, onSubmit, onCellEdit)
  • Check that Table field matches the form you're testing (not a parent table)
  • Test with console.log() statements to confirm script execution
  • Validate field names exist on the form with g_form.hasField()
  • Confirm GlideAjax Script Includes are Active and Client Callable = true

Quick Reference

  • onChange scripts with empty field conditions run on EVERY field change - always specify the field name or use conditional logic in the script
  • Use g_form.getReference() instead of GlideAjax for simple reference field queries - it's 10x faster and doesn't count against API limits
  • The isLoading parameter in onChange scripts is true during form load and when g_form.setValue() is called programmatically
  • Client Scripts inherit security context from the current user - GlideAjax calls will fail if the user lacks read access to queried tables
  • onCellEdit scripts only work in list views and require the cell_edit_roles system property to include the user's role
  • Use g_form.hasField() before any field operations to avoid errors on different form views or when fields are removed by ACLs
  • Scripts with Condition fields that reference current.field_name don't work reliably because the condition evaluates server-side before field changes
  • Global Client Scripts (Table = Global) run on every form in the system and should only contain utility functions, never form-specific logic
  • The order of script execution when multiple scripts exist: onLoad runs by Order field value, onChange runs by Order field, then UI Policies execute