What It Is
GlideForm is ServiceNow's client-side JavaScript API that provides direct manipulation of form elements, field values, and user interface behaviors without requiring a server round-trip. It solves the fundamental problem of creating responsive, dynamic forms that react instantly to user input—something impossible with server-side Business Rules or Script Includes alone. When a user clicks a checkbox and you need three other fields to immediately appear, hide, or change their options, g_form is what makes that happen.
Architecturally, GlideForm lives entirely in the browser—it's client-side JavaScript that executes in the user's browser, not on the ServiceNow server. This means it can provide immediate feedback and dynamic behavior without the latency of server communication. The g_form object is automatically instantiated by ServiceNow when a form loads, giving you a ready-made interface to the form's DOM elements and their associated metadata. It's not just manipulating HTML—it understands ServiceNow's field types, references, choice lists, and form structure.
Under the hood, ServiceNow processes GlideForm code through Client Scripts—JavaScript that runs at specific form lifecycle events like onLoad, onChange, onSubmit, and onCellEdit. When these events fire, ServiceNow's form engine executes your code with the g_form object already populated and ready to use. The platform handles the complexity of field type validation, reference lookups, and UI state management—you just call the methods.
Without GlideForm, you cannot create forms that respond immediately to user interaction. Server-side Business Rules only execute when records are inserted, updated, or deleted—they can't show or hide fields based on what a user is typing right now. UI Policies provide some dynamic behavior, but they're limited to simple show/hide/mandatory logic and can't handle complex calculations, field manipulations, or custom validation messages. GlideForm bridges this gap, enabling the kind of sophisticated user experiences that modern applications demand.
Developers use GlideForm daily for field validation, dynamic field population, conditional UI logic, and custom user interactions. Admins leverage it through Client Scripts to implement business logic that requires immediate user feedback—like calculating totals in real-time or validating complex business rules before form submission. Architects design form interaction patterns around GlideForm capabilities, often pairing it with GlideAjax for server-side data retrieval or Script Includes for complex calculations that need to happen client-side.
GlideForm works closely with GlideAjax for client-server communication, UI Policies for declarative form behavior, and GlideRecord for data operations. While UI Policies handle simple conditional logic without code, GlideForm handles everything UI Policies can't—complex validation, field calculations, custom user interactions, and integration with external systems. GlideAjax becomes essential when your GlideForm code needs server-side data, as client-side scripts can't directly query the database.
How It Works Under the Hood
When ServiceNow renders a form, it builds the g_form object by scanning the form's field metadata from the dictionary, form design, and table schema. The platform creates a JavaScript proxy that sits between your code and the actual DOM elements, handling type conversion, validation, and state management automatically. This is why you can call g_form.setValue('priority', '1') and ServiceNow knows to update both the form field and any dependent choice lists, reference fields, or calculated values.
Client Scripts execute in a carefully orchestrated sequence that developers often don't realize. ServiceNow loads all applicable Client Scripts for a form, then executes them based on their trigger type and timing. The platform maintains a dependency graph of field relationships, so when you change a field value via GlideForm, it automatically triggers any onChange Client Scripts for that field, updates reference qualifiers, refreshes choice lists, and recalculates any dependent fields. This cascading behavior is both powerful and dangerous—a poorly written Client Script can trigger an infinite loop of field updates.
The most critical thing developers miss is that GlideForm operations are asynchronous when they involve server communication. Methods like addOption() or reference field updates may trigger background AJAX calls to refresh choice lists or validate references. Your next line of code might execute before these operations complete, leading to race conditions and unpredictable behavior. ServiceNow provides callback functions for some operations, but many developers don't use them, leading to subtle timing bugs in production.
The Form Lifecycle
- ServiceNow builds the form HTML structure based on the table schema, form design, and user permissions, creating DOM elements for each field.
- The platform instantiates the
g_formobject, mapping each field to its corresponding DOM element and metadata. - All
onLoadClient Scripts execute in order (by order field), with theg_formobject fully available. - User interactions trigger event handlers:
onChangefor field changes,onSubmitfor form submission attempts. - Each GlideForm method call updates the DOM immediately for visual changes, then queues any server-side operations (choice list updates, reference validation) as background AJAX requests.
- Form submission triggers all
onSubmitClient Scripts, which can prevent submission by returning false or display validation messages.
Enjoying this? Get one deep-dive per week.
Join 1,000+ ServiceNow pros — scripts, GlideRecord patterns, Flow Designer techniques, and career moves. Free.
The Core Pattern
function onChange(control, oldValue, newValue, isLoading, isTemplate) {
// Always check if the form is still loading to avoid race conditions
if (isLoading || newValue === '') {
return;
}
// Get the field that changed from the control parameter
var fieldName = control;
if (fieldName == 'priority') {
// High priority incidents require manager approval
if (newValue == '1' || newValue == '2') {
g_form.setMandatory('approval', true);
g_form.setVisible('approval', true);
// Call server-side logic to get default approver
var ga = new GlideAjax('IncidentUtils');
ga.addParam('sysparm_name', 'getDefaultApprover');
ga.addParam('sysparm_caller_id', g_form.getValue('caller_id'));
ga.getXML(populateApprover);
} else {
g_form.setMandatory('approval', false);
g_form.setVisible('approval', false);
g_form.clearValue('approval');
}
}
}var IncidentUtils = Class.create();
IncidentUtils.prototype = Object.extendsObject(AbstractAjaxProcessor, {
getDefaultApprover: function() {
// Get caller's manager from the user record
var callerId = this.getParameter('sysparm_caller_id');
if (!callerId) {
return '';
}
var userGR = new GlideRecord('sys_user');
if (userGR.get(callerId)) {
// Return the manager's sys_id, GlideAjax automatically converts to string
if (userGR.manager) {
return userGR.manager.toString();
}
}
// Fallback to IT manager if no direct manager found
return this._getITManager();
},
_getITManager: function() {
// Private method to get default IT approver
var itManagerGR = new GlideRecord('sys_user');
itManagerGR.addQuery('user_name', 'it.manager');
itManagerGR.query();
if (itManagerGR.next()) {
return itManagerGR.sys_id.toString();
}
return '';
},
type: 'IncidentUtils'
});
// Callback function in Client Script to handle the response
function populateApprover(response) {
var answer = response.responseXML.documentElement.getAttribute('answer');
if (answer) {
g_form.setValue('approval', answer);
}
}Real-World Scenarios
Dynamic Service Catalog Item Pricing
A service catalog item for laptop requests needs to calculate total cost based on selected hardware options and quantity. The price must update immediately as users change their selections, and display a warning if the cost exceeds their department's budget.
function onChange(control, oldValue, newValue, isLoading) {
if (isLoading) return;
var priceFields = ['quantity', 'memory_upgrade', 'ssd_upgrade', 'warranty_extension'];
if (priceFields.indexOf(control) > -1) {
calculateTotalPrice();
}
}
function calculateTotalPrice() {
var basePrice = 1200; // Base laptop price
var quantity = parseInt(g_form.getValue('quantity')) || 1;
var memoryPrice = g_form.getValue('memory_upgrade') == 'true' ? 200 : 0;
var ssdPrice = g_form.getValue('ssd_upgrade') == 'true' ? 150 : 0;
var warrantyPrice = g_form.getValue('warranty_extension') == 'true' ? 100 : 0;
var totalPrice = (basePrice + memoryPrice + ssdPrice + warrantyPrice) * quantity;
// Update the price display field immediately
g_form.setValue('total_price', totalPrice);
// Check budget limit asynchronously
var ga = new GlideAjax('CatalogPricingUtils');
ga.addParam('sysparm_name', 'checkBudgetLimit');
ga.addParam('sysparm_user_id', g_user.userID);
ga.addParam('sysparm_amount', totalPrice);
ga.getXML(handleBudgetCheck);
}var CatalogPricingUtils = Class.create();
CatalogPricingUtils.prototype = Object.extendsObject(AbstractAjaxProcessor, {
checkBudgetLimit: function() {
var userId = this.getParameter('sysparm_user_id');
var amount = parseFloat(this.getParameter('sysparm_amount'));
if (!userId || !amount) {
return 'error';
}
// Get user's department budget from custom table
var userGR = new GlideRecord('sys_user');
if (!userGR.get(userId)) {
return 'error';
}
var deptBudget = new GlideRecord('u_department_budget');
deptBudget.addQuery('department', userGR.department);
deptBudget.addQuery('active', true);
deptBudget.query();
if (deptBudget.next()) {
var budgetLimit = parseFloat(deptBudget.monthly_limit) || 0;
if (amount > budgetLimit) {
return 'over_budget:' + budgetLimit;
}
}
return 'within_budget';
},
type: 'CatalogPricingUtils'
});Watch for timing issues when multiple users are updating catalog items simultaneously—the budget check might be stale by the time the request is submitted. Also, catalog client scripts run in a different context than form client scripts, so some GlideForm methods behave differently. Always test with the actual Service Portal renderer if that's where users will access the catalog.
Multi-Level Reference Field Filtering
Change requests require selecting a business service, then an application within that service, then a configuration item within that application. Each field must filter based on the previous selection and clear dependent fields when parent selections change.
function onChange(control, oldValue, newValue, isLoading) {
if (isLoading) return;
if (control == 'business_service') {
// Clear dependent fields when business service changes
g_form.clearValue('u_application');
g_form.clearValue('cmdb_ci');
if (newValue) {
// Set reference qualifier for applications
g_form.addFilter('u_application', 'u_business_service=' + newValue);
g_form.setReadOnly('u_application', false);
} else {
g_form.clearFilter('u_application');
g_form.setReadOnly('u_application', true);
}
// Always disable CI field until application is selected
g_form.setReadOnly('cmdb_ci', true);
}
else if (control == 'u_application') {
g_form.clearValue('cmdb_ci');
if (newValue) {
// Filter CIs to only show those related to selected application
var filter = 'u_application=' + newValue + '^operational_status!=6';
g_form.addFilter('cmdb_ci', filter);
g_form.setReadOnly('cmdb_ci', false);
} else {
g_form.clearFilter('cmdb_ci');
g_form.setReadOnly('cmdb_ci', true);
}
}
}var ChangeRequestUtils = Class.create();
ChangeRequestUtils.prototype = Object.extendsObject(AbstractAjaxProcessor, {
getApplicationsByService: function() {
var serviceId = this.getParameter('sysparm_service_id');
var applications = [];
if (serviceId) {
var appGR = new GlideRecord('u_application');
appGR.addQuery('u_business_service', serviceId);
appGR.addQuery('active', true);
appGR.orderBy('name');
appGR.query();
while (appGR.next()) {
applications.push({
value: appGR.sys_id.toString(),
label: appGR.name.toString()
});
}
}
return new global.JSON().encode(applications);
},
validateCIRelationship: function() {
var ciId = this.getParameter('sysparm_ci_id');
var appId = this.getParameter('sysparm_app_id');
// Verify the CI actually belongs to the selected application
var ciGR = new GlideRecord('cmdb_ci');
if (ciGR.get(ciId)) {
return (ciGR.u_application.toString() == appId);
}
return false;
},
type: 'ChangeRequestUtils'
});Reference qualifiers applied via addFilter() don't validate existing field values—users can still submit forms with invalid combinations if they don't re-select the filtered fields. Always add server-side validation in a Business Rule to catch these cases. Also, clearing filters with clearFilter() doesn't automatically refresh the field's choice list in some browsers.
Complex Form Validation with Custom Messages
Problem records require different mandatory fields based on the problem category, and the validation rules are complex enough that standard mandatory field configuration isn't sufficient. Users need immediate feedback about what's missing and why.
function onSubmit() {
var category = g_form.getValue('problem_category');
var isValid = true;
// Clear any previous validation messages
g_form.hideFieldMsg('problem_category', true);
g_form.hideFieldMsg('u_business_impact', true);
g_form.hideFieldMsg('u_technical_cause', true);
if (category == 'business_process') {
// Business process problems require impact assessment
if (!g_form.getValue('u_business_impact')) {
g_form.showFieldMsg('u_business_impact',
'Business impact assessment is required for process-related problems',
'error');
isValid = false;
}
// Must have at least 3 affected users for business process issues
var affectedUsers = parseInt(g_form.getValue('u_affected_user_count')) || 0;
if (affectedUsers < 3) {
g_form.showFieldMsg('u_affected_user_count',
'Business process problems must affect at least 3 users',
'error');
isValid = false;
}
}
else if (category == 'technical_infrastructure') {
// Technical problems require root cause analysis
if (!g_form.getValue('u_technical_cause')) {
g_form.showFieldMsg('u_technical_cause',
'Technical cause analysis is mandatory for infrastructure problems',
'error');
isValid = false;
}
// Validate CI is actually infrastructure type
var ciType = g_form.getReference('cmdb_ci', function(ref) {
if (ref && ref.sys_class_name) {
var className = ref.sys_class_name;
if (!className.startsWith('cmdb_ci_server') && !className.startsWith('cmdb_ci_netgear')) {
g_form.showFieldMsg('cmdb_ci',
'Selected CI must be infrastructure component for this category',
'error');
isValid = false;
}
}
});
}
return isValid;
}var ProblemValidationUtils = Class.create();
ProblemValidationUtils.prototype = Object.extendsObject(AbstractAjaxProcessor, {
validateProblemCategory: function() {
var problemId = this.getParameter('sysparm_problem_id');
var category = this.getParameter('sysparm_category');
var problemGR = new GlideRecord('problem');
if (!problemGR.get(problemId)) {
return 'invalid_problem';
}
// Check if category change is allowed based on current state
if (problemGR.state >= 3) { // Known Error state or later
return 'category_locked';
}
// Validate category against existing related incidents
var incidentGR = new GlideRecord('incident');
incidentGR.addQuery('problem_id', problemId);
incidentGR.query();
var conflictingCategories = [];
while (incidentGR.next()) {
if (incidentGR.category != category) {
conflictingCategories.push(incidentGR.number.toString());
}
}
if (conflictingCategories.length > 0) {
return 'category_conflict:' + conflictingCategories.join(',');
}
return 'valid';
},
type: 'ProblemValidationUtils'
});The onSubmit function runs synchronously, but getReference() is asynchronous—the callback might not execute before the form submits. For complex validation that requires server-side data, use GlideAjax calls in onChange events to pre-validate and store results in hidden fields, then check those fields in onSubmit. Field messages can stack up if you don't clear them properly, confusing users with outdated error messages.
The Classic Mistake
Using g_form methods inside loops without checking field existence first, causing console errors and broken form interactions.
// Client Script - onLoad
function onLoad() {
var fields = ['assignment_group', 'assigned_to', 'priority', 'impact', 'urgency', 'category'];
// Loop through and set all fields to mandatory
for (var i = 0; i < fields.length; i++) {
g_form.setMandatory(fields[i], true);
g_form.setReadOnly(fields[i], false);
// Set default values based on current user
if (fields[i] == 'assigned_to') {
g_form.setValue(fields[i], g_user.userID);
}
}
g_form.addInfoMessage('Form initialized successfully');
}This fails because not all forms contain every field in the array, and g_form methods don't validate field existence before executing. When the script encounters a field that doesn't exist on the current form, you'll see "Field 'fieldname' does not exist" errors in the browser console, and subsequent form interactions become unreliable. ServiceNow's client-side form engine tries to find DOM elements for non-existent fields, which corrupts the internal field registry and can cause legitimate fields to stop responding to user input.
// Client Script - onLoad
function onLoad() {
var fields = ['assignment_group', 'assigned_to', 'priority', 'impact', 'urgency', 'category'];
// Only manipulate fields that exist on this form
for (var i = 0; i < fields.length; i++) {
if (g_form.getControl(fields[i])) {
g_form.setMandatory(fields[i], true);
g_form.setReadOnly(fields[i], false);
// Set default values based on current user
if (fields[i] == 'assigned_to' && !g_form.getValue(fields[i])) {
g_form.setValue(fields[i], g_user.userID);
}
}
}
g_form.addInfoMessage('Form initialized successfully');
}Always call g_form.getControl(fieldName) to verify field existence before any other g_form operation on that field.
Performance Rules
- Never call
g_form.getValue()more than 50 times in a single script execution - each call triggers DOM traversal. Beyond this threshold, form responsiveness degrades noticeably and users report "sluggish" behavior when clicking fields or buttons. - Avoid
g_form.addInfoMessage()oraddErrorMessage()inside onChange scripts that fire frequently. More than 3 messages per second creates a message queue backlog that blocks form submission and causes the browser to consume excessive memory. - Cache reference field display values using variables instead of repeatedly calling
g_form.getDisplayValue()for the same field. Each call makes a synchronous AJAX request, and 10+ calls within 5 seconds triggers rate limiting that freezes reference field lookups. - Never use
g_form.setValue()on more than 15 fields simultaneously in onLoad scripts. ServiceNow's change tracking mechanism creates individual audit entries for each setValue operation, causing database lock contention on thesys_audittable and 30+ second form save times. - Avoid calling
g_form.getReference()with callback functions inside loops or recursive functions. Each call establishes a separate server connection, and 20+ concurrent calls exhaust the browser's connection pool, causing subsequent AJAX requests to time out. - Don't manipulate choice list options using
g_form.clearOptions()andaddOption()for fields with more than 100 choices. The DOM manipulation creates layout thrashing, and users experience 5-10 second delays when clicking dropdown fields. - Never call
g_form.submit()from within onSubmit client scripts - this creates infinite submission loops that crash the browser tab and generate hundreds of duplicate records in the database. - Limit
g_form.showFieldMsg()to essential validation errors only. Displaying field messages on more than 8 fields simultaneously causes the form layout engine to recalculate positioning continuously, consuming 100% CPU and making scrolling impossible.
Side Effects & Platform Behavior
- Every
g_form.setValue()call triggers onChange client scripts for that field, which can cascade and trigger Business Rules when the form is saved. Thesys_script_clientexecution order becomes unpredictable with multiple setValue operations. - Form messages created by
addInfoMessage()andaddErrorMessage()are logged to the browser's session storage and persist across page refreshes until explicitly cleared or the browser tab is closed. - Reference field operations like
getReference()create entries in thesys_ajaxtable and consume GlideRecord queries against your instance's allocation, potentially affecting performance monitoring dashboards. - Field visibility changes using
setVisible()are not respected by Data Policies or UI Policies that run after your client script, and ACLs always take precedence over client-side visibility settings. - Using
g_formmethods in Service Portal widgets breaks completely - Service Portal uses Angular scope variables instead of the GlideForm API, causing "g_form is not defined" errors. - Form submission via
g_form.submit()bypasses mandatory field validation and can create incomplete records, but still triggers all configured Business Rules, Workflows, and Notifications for the target table. - Choice list modifications using
addOption()only affect the current form session - the changes are not saved to thesys_choicetable and disappear when the user navigates away or refreshes the page. - Field-level security enforced by
setReadOnly()is cosmetic only - malicious users can bypass it with browser developer tools, and server-side ACLs are the only true security enforcement. - Related list refreshes triggered by
g_form.save()cause all related list client scripts to re-execute, potentially creating recursive loops if those scripts also call save operations or trigger form field changes. - Dictionary overrides and personalization settings in user preferences can interfere with
g_formfield manipulations, causing scripts to work for developers but fail for end users who have customized their form layouts.
Debugging When It Breaks
The most common failure symptom is fields becoming unresponsive to user input or displaying stale data after form interactions. Users report that dropdown fields won't open, reference fields show "Loading..." permanently, or mandatory field indicators appear and disappear randomly. From a developer perspective, you'll typically see JavaScript errors in the browser console, particularly "Cannot read property of undefined" when trying to access field controls that don't exist.
Always check the browser's Developer Console first - press F12 and look at the Console tab for red error messages that mention field names or g_form. For server-side debugging, navigate to System Logs > System Log > All and filter by your user name to see AJAX call failures. The Script Debugger (System Definition > Script Debugger) won't help with GlideForm issues since it's client-side only, but check it if your client scripts make server calls via getReference() or GlideAjax.
Common error patterns include "Field 'fieldname' does not exist on this form" (you're manipulating fields not present), "g_form.getValue(...) is not a function" (calling GlideForm methods outside of form context), and "Maximum call stack size exceeded" (recursive setValue() calls triggering onChange scripts). Network tab errors showing 500 responses to /api/now/form endpoints indicate server-side processing failures when your client scripts trigger business rule executions.
Quick diagnostic checklist:
- Verify field existence with
g_form.getControl(fieldName) != nullbefore any field operations - Check if your script runs on the correct form type (standard forms vs Service Portal vs mobile)
- Confirm client script conditions and table restrictions match your test scenario
- Test with admin privileges to rule out ACL interference with field visibility or editability
- Disable other client scripts temporarily to isolate conflicts between multiple scripts
Quick Reference
- Use
g_form.getDisplayValue()for reference fields to get human-readable text,getValue()returns sys_ids - Field messages set by
showFieldMsg()persist until explicitly cleared withhideFieldMsg() - Call
g_form.isNewRecord()to differentiate between insert and update operations in your client scripts - Choice field options added with
addOption()require both a value and label parameter:addOption(value, label) - Form sections can be hidden/shown using
g_form.setSectionDisplay()with the exact section name from the form layout - Reference field callbacks in
getReference()receive a GlideRecord object, not a JavaScript object - usegr.getValue('field') - Multiple choice fields require
getValue()to be split on commas to access individual selected values - Use
g_form.getTableName()to make client scripts table-agnostic when working with extended tables - Encrypted fields always return empty strings from
getValue()in client scripts for security reasons - Client script execution order: onLoad → onChange → onSubmit, but UI Policies can override field states set by client scripts