What are Client Scripts?
Client Scripts are JavaScript functions that run in the user's browser when they interact with a ServiceNow form. Unlike Business Rules which run on the server when records are saved, Client Scripts execute on the client side — giving you the ability to respond to user actions immediately, without a round trip to the server.
Common Client Script use cases:
- Show or hide fields based on the value of another field
- Make a field mandatory based on conditions
- Validate form data before saving
- Populate a field based on another field's value
- Fetch server-side data asynchronously and display it on the form
- Show informational messages next to fields
Client Scripts are accessed at System Definition > Client Scripts in the navigator. Each Client Script is associated with a table and fires on forms for that table.
The four Client Script types
onLoad — fires when the form loads
The onLoad script fires once when the form is fully loaded in the browser. Use it to:
- Set the initial visibility or mandatory state of fields based on existing values
- Fetch additional data from the server via GlideAjax that you need available on the form
- Set default values for fields that cannot be set through field defaults
- Disable fields or sections that should not be editable in certain states
function onLoad() {
var state = g_form.getValue('state');
// Hide resolution fields unless the incident is being resolved
if (state != '6') {
g_form.setVisible('close_code', false);
g_form.setVisible('close_notes', false);
g_form.setVisible('resolution_code', false);
}
// Make the work notes mandatory for P1 incidents
if (g_form.getValue('priority') == '1') {
g_form.setMandatory('work_notes', true);
g_form.showFieldMsg('work_notes', 'Work notes required for P1 incidents', 'info');
}
}
Important: onLoad fires with the values that exist when the form opens. It does not re-run when fields change. Logic that should respond to field changes belongs in onChange, not onLoad.
onChange — fires when a specific field changes
The onChange script fires when the field specified in the Field name setting on the Client Script record changes its value. It receives four arguments:
control— the DOM element of the field that changedoldValue— the previous value before the changenewValue— the new value after the changeisLoading— true when the script fires during initial form load
function onChange(control, oldValue, newValue, isLoading) {
// Critical: always check isLoading and return early
// onChange fires once on load with isLoading = true
// Without this check, your script runs on load AND on change
if (isLoading) return;
// Also handle empty string (field cleared)
if (newValue === '') return;
// Your logic here — only runs on actual field changes
if (newValue == '6') { // State = Resolved
g_form.setVisible('resolution_code', true);
g_form.setVisible('close_notes', true);
g_form.setMandatory('resolution_code', true);
g_form.setMandatory('close_notes', true);
} else {
g_form.setVisible('resolution_code', false);
g_form.setVisible('close_notes', false);
g_form.setMandatory('resolution_code', false);
g_form.setMandatory('close_notes', false);
}
}
The isLoading check is not optional — it is the most common Client Script mistake. Every onChange script fires on load with isLoading = true. Without the early return, your script runs twice: once when the form loads (with the existing value), and once when the user actually changes the field.
onChange with a reference field
When the field being watched is a reference field (like assigned_to or assignment_group), newValue contains the sys_id of the selected record:
function onChange(control, oldValue, newValue, isLoading) {
if (isLoading) return;
if (!newValue) {
// Field was cleared
g_form.clearValue('location');
return;
}
// newValue is the sys_id of the selected user
// Fetch the user's location via GlideAjax
var ga = new GlideAjax('UserAjaxUtils');
ga.addParam('sysparm_name', 'getUserLocation');
ga.addParam('sysparm_user_id', newValue);
ga.getXMLAnswer(function(answer) {
if (answer) {
g_form.setValue('location', answer);
}
});
}
Reference field watch: the onChange for a reference field fires when the user selects a record from the reference picker. The newValue is the sys_id. Use g_form.getReference('field_name', callback) if you need to access the full referenced record object on the client side.
onSubmit — fires on form submission
The onSubmit script fires when the user clicks Save or submits the form. It can cancel the submission by returning false. Use it for client-side validation that cannot be handled by mandatory field settings alone:
function onSubmit() {
var state = g_form.getValue('state');
var closeNotes = g_form.getValue('close_notes');
var priority = g_form.getValue('priority');
// Validate: resolution notes required when resolving
if (state == '6' && !closeNotes) {
g_form.showFieldMsg('close_notes', 'Resolution notes are required to resolve this incident.', 'error');
g_form.setMandatory('close_notes', true);
return false; // Cancel the save
}
// Validate: P1 incidents require manager approval comment
if (priority == '1' && state == '6') {
var workNotes = g_form.getValue('work_notes');
if (!workNotes || workNotes.length < 50) {
alert('P1 incidents require detailed work notes (minimum 50 characters) before resolution.');
return false;
}
}
// Allow submission
// return true is implicit — you only need to return false to cancel
}
Key points about onSubmit:
- Return false to cancel the save — the form stays open and the user can correct errors
- Not returning anything (or returning true) allows the save to proceed
- Use
g_form.showFieldMsg()to display contextual error messages next to fields - onSubmit does not fire on list edits — only on form saves
- onSubmit fires for all save actions: Save, Update, and other submit buttons
onCellEdit — fires on list view cell edits
The onCellEdit script fires when a cell is edited directly in a list view (when list editing is enabled). It does not fire on forms. Less commonly used, but useful when you need validation or dependent field population in list editing scenarios:
function onCellEdit(sysIDs, table, oldValues, newValue, callback) {
// sysIDs: array of sys_ids of the records being edited
// table: the table name
// oldValues: array of old values (one per sysID)
// newValue: the new value entered
// callback: must be called when validation is complete
// Example: prevent state change to resolved without close notes
if (newValue == '6') { // State = Resolved
var message = 'Cannot resolve from list view — use the record form to add resolution notes.';
callback(false, message);
} else {
callback(true); // Allow the edit
}
}
The callback pattern is unique to onCellEdit. You must call callback(true) to allow the edit or callback(false, 'message') to cancel it with an error message.
The g_form API — complete reference
g_form is the client-side API for interacting with form fields. Every Client Script uses it.
Reading field values
// Get the raw value (sys_id for reference fields, code value for choice fields)
var state = g_form.getValue('state'); // Returns '1', '2', '6' etc.
var assignedTo = g_form.getValue('assigned_to'); // Returns sys_id
// Get the display value (human-readable label)
var stateLabel = g_form.getDisplayValue('state'); // Returns 'New', 'Open', 'Resolved' etc.
var userName = g_form.getDisplayValue('assigned_to'); // Returns 'John Smith'
// Check if a field has a value
if (g_form.getValue('priority')) { /* not empty */ }
if (!g_form.getValue('close_notes')) { /* empty */ }
Setting field values
// Set a simple field value
g_form.setValue('priority', '1'); // Set priority to Critical
// Set a reference field — pass sys_id and optionally the display value
g_form.setValue('assigned_to', 'USER_SYS_ID', 'John Smith');
// Clear a field
g_form.clearValue('field_name');
// Set a field to read-only
g_form.setReadOnly('field_name', true);
g_form.setReadOnly('field_name', false); // Make editable again
Visibility and mandatory
// Show/hide fields
g_form.setVisible('field_name', true);
g_form.setVisible('field_name', false);
// Make mandatory
g_form.setMandatory('field_name', true);
g_form.setMandatory('field_name', false);
// Combine visibility and mandatory
g_form.setVisible('close_notes', true);
g_form.setMandatory('close_notes', true);
Messages
// Show a message next to a field
g_form.showFieldMsg('field_name', 'Message text', 'info'); // blue info
g_form.showFieldMsg('field_name', 'Message text', 'warning'); // yellow warning
g_form.showFieldMsg('field_name', 'Message text', 'error'); // red error
// Clear a field message
g_form.hideFieldMsg('field_name');
g_form.hideFieldMsg('field_name', true); // also clears mandatory style
// Form-level messages (top of form)
g_form.addInfoMessage('Form-level informational message');
g_form.addErrorMessage('Form-level error message');
Section visibility
// Show/hide entire form sections
g_form.setSectionDisplay('Resolution Information', false); // hide by section name
g_form.setSectionDisplay('section_name', true); // show
Getting reference field objects
// Asynchronously get a referenced record's details
g_form.getReference('assigned_to', function(ref) {
// ref is a GlideRecord-like object with the referenced user's data
var dept = ref.department.getDisplayValue();
g_form.setValue('department', dept);
});
Fetching server data from a Client Script — GlideAjax
Client Scripts cannot directly query the database — all database access must go through the server. The correct pattern is a client-callable Script Include called via GlideAjax.
Never use synchronous queries from Client Scripts. The synchronous GlideRecord API is deprecated on the client side and will freeze the browser. Always use the asynchronous GlideAjax pattern:
function onChange(control, oldValue, newValue, isLoading) {
if (isLoading || !newValue) return;
// Correct: async GlideAjax
var ga = new GlideAjax('IncidentAjaxUtils');
ga.addParam('sysparm_name', 'getGroupDetails');
ga.addParam('sysparm_group_id', newValue);
ga.getXMLAnswer(function(answer) {
var data = JSON.parse(answer);
g_form.setValue('location', data.location);
g_form.setValue('manager', data.manager, data.managerName);
});
}
For building the server-side Script Include that GlideAjax calls, see the complete guide to Script Includes and the GlideAjax guide.
Client Scripts vs UI Policies
Many form behaviours that developers write Client Scripts for can be handled by UI Policies — a no-code alternative that is easier to maintain and more performant.
Use UI Policies when:
- You need to show/hide, make mandatory, or make read-only based on a simple field condition
- The condition does not require server data
- You want the rule visible and maintainable by admins without reading JavaScript
Use Client Scripts when:
- The logic is too complex for UI Policy conditions
- You need to fetch server data (GlideAjax)
- You need to set a field's value (not just its visibility)
- You need to show custom messages with g_form.showFieldMsg()
- You need to cancel form submission (onSubmit)
When both would work, prefer UI Policies. They are evaluated by the platform engine, not interpreted JavaScript, and they do not require a developer to understand the intent.
Performance best practices for Client Scripts
Always use isLoading guard in onChange
function onChange(control, oldValue, newValue, isLoading) {
if (isLoading) return; // Required — not optional
// ...
}
Avoid synchronous GlideRecord on the client side
Synchronous client-side GlideRecord is deprecated and causes browser freezes. Use GlideAjax instead for any server data you need.
Set Active to false when not in use
Inactive Client Scripts do not load at all. If a Client Script is only needed in certain conditions, consider making it conditional with a View filter or deactivating it when not needed, rather than having it load on every form open.
Use Views to scope Client Scripts
Client Scripts can be scoped to specific form views. If a script only applies to the Incident form in the ITSM Portal view, scope it to that view rather than applying it globally. This reduces the number of scripts that load on every form.
Minimise round trips
Each GlideAjax call is a server round trip. If you need multiple pieces of server data, combine them into a single GlideAjax call that returns a JSON object with everything you need — rather than making separate calls for each value.
Common mistakes
Mistake 1 — Missing isLoading check
Already covered above, but worth repeating: every onChange script that does not check isLoading will fire twice on form load and produce unexpected behaviour.
Mistake 2 — Using alert() for validation messages
// Bad — blocks the browser, ugly, not accessible
alert('Please fill in the resolution notes');
// Good — contextual, dismissible, styled correctly
g_form.showFieldMsg('resolution_notes', 'Resolution notes required', 'error');
g_form.addErrorMessage('Please complete required fields before saving.');
Mistake 3 — Synchronous GlideRecord in Client Scripts
// Wrong — synchronous, deprecated
var gr = new GlideRecord('sys_user');
gr.get(g_form.getValue('caller_id'));
g_form.setValue('department', gr.getValue('department'));
// Right — async GlideAjax
var ga = new GlideAjax('UserAjaxUtils');
ga.addParam('sysparm_name', 'getUserDepartment');
ga.addParam('sysparm_user_id', g_form.getValue('caller_id'));
ga.getXMLAnswer(function(department) {
g_form.setValue('department', department);
});
Mistake 4 — Returning false from onLoad
Returning false from onLoad does not cancel anything — it has no effect. Returning false only works in onSubmit to cancel the save.
Mistake 5 — Not handling the empty value case in onChange
When a user clears a reference field, newValue is an empty string. Your script needs to handle this case explicitly:
For server-side debugging of the Script Includes that your Client Scripts call, see the complete debugging guide.
Client Scripts and the broader scripting picture
Client Scripts interact with several other parts of the ServiceNow scripting ecosystem:
- Script Includes — server-side libraries called by Client Scripts via GlideAjax
- GlideAjax — the mechanism for async server calls from Client Scripts
- Business Rules — server-side counterpart that fires on record saves
- Debugging scripts — how to debug both client and server-side issues