Skip to main content
Version: 4.11.0

Catalog Client Scripts

Guide for building ServiceNow Catalog Client Scripts (catalog_script_client) for service catalog items, record producers, and variable sets. These client-side scripts run in response to catalog form events such as onLoad, onChange, or onSubmit, enabling dynamic form behavior like field validation, visibility toggling, auto-populating fields from the current user, and user feedback messages.


Properties​

For the complete property reference including all properties, types, and detailed descriptions, see the catalogclientscript-api documentation.

Script Types​

onLoad​

Runs when the form loads. Use for initial setup (field states, defaults, visibility).

onChange​

Runs when a specific variable changes. Always guard with if (isLoading) return; to prevent execution during form load.

onSubmit​

Runs on form submission. Return false to block submission. Avoid GlideAjax here — async calls won't complete before the form submits.


g_form API Reference​

MethodDescription
getValue(fieldName)Get variable value
setValue(fieldName, value)Set variable value
setDisplay(fieldName, display)Show/hide variable
setMandatory(fieldName, mandatory)Set mandatory state
setReadOnly(fieldName, readOnly)Set read-only state
clearValue(fieldName)Clear variable value
hasField(fieldName)Check if field exists
showFieldMsg(fieldName, message, type, scrollForm)Show field message
hideFieldMsg(fieldName, clearAll)Hide field message
addErrorMessage(message)Add banner error message
clearOptions(fieldName)Clear all select options
addOption(fieldName, value, label)Add a select option
getReference(fieldName, callback)Get referenced record (legacy)

Note on getReference: Legacy convenience method. Works for simple lookups but GlideAjax is preferred for complex server-side logic. May make synchronous calls in some versions, which can freeze the UI.


GlideAjax​

Use GlideAjax to call server-side Script Includes from client scripts. The client sends a request, the Script Include processes it, and returns a result via a callback.

GlideAjax Types — Quick Decision Guide​

MethodExecutionUse WhenAvoid When
getXMLAnswer()AsyncSimple lookups, returning a single value/stringYou need the full XML response object
getXML()AsyncNeed full XML response, complex response parsingSimple value returns (use getXMLAnswer)
getXMLWait()SyncAlmost never — legacy/global scope onlyScoped apps, any production code

GlideAjax Parameter Rules​

All custom parameters must start with sysparm_. The first addParam call must always be sysparm_name with the method name.

ga.addParam("sysparm_name", "methodName"); // REQUIRED: always first
ga.addParam("sysparm_user_id", userSysId); // Custom param: prefix with sysparm_
ga.addParam("sysparm_category", selectedCat); // Custom param: prefix with sysparm_

Script Include (Server-Side Companion)​

Every GlideAjax call requires a corresponding Script Include on the server. The Script Include must extend AbstractAjaxProcessor and be marked Client Callable.

Script Include Setup Checklist​

PropertyValue
NameMust match the string in new GlideAjax('ClassName')
Client callableChecked (required for GlideAjax access)
Extendsglobal.AbstractAjaxProcessor
Retrieve paramsUse this.getParameter('sysparm_param_name')
Return dataUse return (simple string) or return JSON.stringify(obj) for objects

Script Include Security​

  • Client callable = true: Only check this when the method is explicitly needed from the client.
  • Access controls: Runs in the logged-in user's session context. ACLs still apply to GlideRecord queries.
  • Input validation: Always validate parameters from this.getParameter(). Never trust client-side input.

Scripts on Variable Sets​

Scope scripts to a variable set using variableSet and appliesTo: 'set' so they apply to all catalog items using that set. Always use hasField() checks since the variable may not exist on every item that includes the set.

Execution Order Note​

When multiple variable sets are attached to a catalog item, scripts execute in the order the variable sets are listed on the item. If both a variable set script and an item-level script target the same variable, the item-level script runs last and takes precedence.


Catalog Client Script vs Standard Client Script​

AspectCatalog Client ScriptStandard Client Script
ScopeCatalog item or variable setTable (e.g., Incident)
onChange targetLinks to a variableLinks to a field
ContextCatalog ordering, RITM, Catalog Task formsTable forms
Variable accessDirect by variable nameUse variables.variable_name prefix
Applies toitem or setSpecific table

Best Practices​

  • Guard onChange: Always start with if (isLoading) return; to prevent execution on form load.
  • Use hasField() in variable set scripts: The variable may not exist on every catalog item using the set.
  • Avoid GlideAjax in onSubmit: Async calls won't complete before submission. Use onSubmit only for synchronous client-side validation.
  • Use g_form APIs, not DOM: Never use document.getElementById() or jQuery — it breaks across UI versions (UI16, Workspace, Portal).
  • Prefix for conflicts: If a variable name conflicts with a table field name, use variables.variable_name to reference the catalog variable.
  • Return JSON from Script Includes: Use JSON.stringify() on the server and JSON.parse() on the client for structured data.
  • Validate inputs in Script Includes: Never trust client-side parameters. Check for empty values, validate sys_id formats, and handle edge cases.
  • Use Now.include() for shared code: Extract reusable client logic into UI Scripts and load with Now.include('script_name') for maintainability.
  • Prefer getXMLAnswer over getXML: Simpler callback, less parsing code. Use getXML only when you need the full XML response.
  • Never use getXMLWait(): Synchronous, freezes the UI, not available in scoped apps. Always use async patterns.

Examples​

Catalog Client Script -- onLoad​

import { CatalogClientScript } from "@servicenow/sdk/core";
import { laptopRequest } from "../catalog-items/laptop-request.now";

CatalogClientScript({
$id: Now.ID["laptop_onload"],
name: "Laptop Request - OnLoad",
script: Now.include("../../client/laptop-onload.js"),
type: "onLoad",
catalogItem: laptopRequest,
active: true,
appliesOnCatalogItemView: true
});

laptop-onload.js:

function onLoad() {
g_form.setReadOnly("estimated_cost", true);
g_form.setValue("estimated_cost", "$0");
g_form.setMandatory("justification", true);
}

Catalog Client Script -- onChange​

import { CatalogClientScript } from "@servicenow/sdk/core"
import { laptopRequest } from '../catalog-items/laptop-request.now'

CatalogClientScript({
$id: Now.ID["laptop_type_change"],
name: "Laptop Type - onChange",
script: Now.include("../../client/laptop-type-change.js"),
type: "onChange",
catalogItem: laptopRequest,
variableName: laptopRequest.variables.laptopType,
active: true
});

laptop-type-change.js:

function onChange(control, oldValue, newValue, isLoading) {
if (isLoading) return; // Always guard against initial load

if (newValue === "developer") {
g_form.setDisplay("accessories", true);
} else {
g_form.setDisplay("accessories", false);
g_form.clearValue("accessories");
}
}

Catalog Client Script -- onSubmit Validation​

import { CatalogClientScript } from "@servicenow/sdk/core"
import { laptopRequest } from '../catalog-items/laptop-request.now'

CatalogClientScript({
$id: Now.ID["laptop_validation"],
name: "Laptop Request - Validation",
script: Now.include("../../client/laptop-validation.js"),
type: "onSubmit",
catalogItem: laptopRequest,
active: true
});

laptop-validation.js:

function onSubmit() {
var justification = (g_form.getValue("justification") || "").trim();

if (justification.length < 20) {
g_form.showFieldMsg("justification", "Please provide at least 20 characters.", "error", true);
g_form.addErrorMessage("Justification is too short.");
return false;
}

return true;
}

Catalog Client Script -- onChange with GlideAjax​

import { CatalogClientScript } from "@servicenow/sdk/core"
import { equipmentRepairItem } from '../catalog-items/equipment-repair'

CatalogClientScript({
$id: Now.ID["asset_tag_lookup"],
name: "Asset Tag - Warranty Lookup",
script: Now.include("../../client/asset-tag-lookup.js"),
type: "onChange",
catalogItem: equipmentRepairItem,
variableName: equipmentRepairItem.variables.asset_tag,
active: true
});

asset-tag-lookup.js:

function onChange(control, oldValue, newValue, isLoading) {
if (isLoading) return;

if (!newValue) {
g_form.clearValue("warranty_status");
return;
}

var ga = new GlideAjax("global.AssetLookupUtil");
ga.addParam("sysparm_name", "getWarrantyStatus");
ga.addParam("sysparm_asset_tag", newValue);
ga.getXMLAnswer(function (response) {
if (!response) return;
var info = JSON.parse(response);
g_form.setValue("warranty_status", info.status);
});
}

Catalog Client Script -- Scoped to Variable Set​

import { CatalogClientScript } from "@servicenow/sdk/core";
import { requesterInfoSet } from "./variable-sets/requester-info-set.now";

CatalogClientScript({
$id: Now.ID["department_change_script"],
name: "Department Change - Clear Manager",
type: "onChange",
variableSet: requesterInfoSet,
appliesTo: "set",
variableName: requesterInfoSet.variables.department,
script: Now.include("../../client/department-change.js"),
active: true
});

department-change.js:

function onChange(control, oldValue, newValue, isLoading) {
if (isLoading) return;
g_form.clearValue("manager");
if (!newValue) return;
g_form.showFieldMsg("manager", "Please select a manager from the new department", "info", false);
}

GlideAjax -- Dynamic Options Based on Selection​

Client script (onChange on 'department' variable):

function onChange(control, oldValue, newValue, isLoading) {
if (isLoading) return;

g_form.clearOptions("category");
g_form.addOption("category", "", "-- Select --");

if (!newValue) return;

var ga = new GlideAjax("CatalogOptionLoader");
ga.addParam("sysparm_name", "getCategoriesByDept");
ga.addParam("sysparm_department", newValue);

ga.getXMLAnswer(function (answer) {
if (!answer) return;
var categories = JSON.parse(answer);
categories.forEach(function (cat) {
g_form.addOption("category", cat.value, cat.label);
});
});
}

Script Include (CatalogOptionLoader, Client callable = true):

var CatalogOptionLoader = Class.create();
CatalogOptionLoader.prototype = Object.extendsObject(global.AbstractAjaxProcessor, {
getCategoriesByDept: function () {
var deptId = this.getParameter("sysparm_department");
var categories = [];

var gr = new GlideRecord("sc_category");
gr.addQuery("department", deptId);
gr.addQuery("active", true);
gr.orderBy("title");
gr.query();

while (gr.next()) {
categories.push({ value: gr.getUniqueValue(), label: gr.getValue("title") });
}
return JSON.stringify(categories);
},

type: "CatalogOptionLoader"
});

GlideAjax -- Server-Side Validation (getXML)​

Client script (onChange on 'asset_tag' variable):

function onChange(control, oldValue, newValue, isLoading) {
if (isLoading) return;
g_form.hideFieldMsg("asset_tag", true);

if (!newValue) {
g_form.clearValue("configuration_item");
return;
}

var ga = new GlideAjax("AssetValidator");
ga.addParam("sysparm_name", "validateAssetTag");
ga.addParam("sysparm_asset_tag", newValue);

ga.getXML(function (response) {
var answer = response.responseXML.documentElement.getAttribute("answer");
if (!answer) {
g_form.showFieldMsg("asset_tag", "Unable to validate. Try again.", "error");
return;
}

var result = JSON.parse(answer);
if (result.found) {
g_form.setValue("configuration_item", result.ci_sys_id);
g_form.showFieldMsg("asset_tag", "Found: " + result.ci_name, "info");
} else {
g_form.clearValue("configuration_item");
g_form.showFieldMsg("asset_tag", "Asset tag not found in CMDB.", "error");
}
});
}

Script Include (AssetValidator, Client callable = true):

var AssetValidator = Class.create();
AssetValidator.prototype = Object.extendsObject(global.AbstractAjaxProcessor, {
validateAssetTag: function () {
var assetTag = this.getParameter("sysparm_asset_tag");

if (!assetTag) {
return JSON.stringify({ found: false, error: "No asset tag provided" });
}

var gr = new GlideRecord("cmdb_ci");
gr.addQuery("asset_tag", assetTag);
gr.setLimit(1);
gr.query();

if (gr.next()) {
return JSON.stringify({
found: true,
ci_sys_id: gr.getUniqueValue(),
ci_name: gr.getDisplayValue("name"),
ci_class: gr.getDisplayValue("sys_class_name")
});
}
return JSON.stringify({ found: false });
},

type: "AssetValidator"
});

Script Include -- Multi-Method Pattern​

var CatalogUtils = Class.create();
CatalogUtils.prototype = Object.extendsObject(global.AbstractAjaxProcessor, {
getItemPrice: function () {
var itemId = this.getParameter("sysparm_item_id");
var gr = new GlideRecord("sc_cat_item");
if (gr.get(itemId)) {
return gr.getValue("price");
}
return "0";
},

getManagerName: function () {
var userId = this.getParameter("sysparm_user_id");
var gr = new GlideRecord("sys_user");
if (gr.get(userId)) {
return JSON.stringify({
manager_sys_id: gr.getValue("manager"),
manager_name: gr.getDisplayValue("manager"),
department: gr.getDisplayValue("department")
});
}
return JSON.stringify({ error: "User not found" });
},

type: "CatalogUtils"
});

Script Include -- Input Validation​

getUserInfo: function() {
var userId = this.getParameter('sysparm_user_id');

// Validate: check it looks like a sys_id
if (!userId || userId.length !== 32) {
return JSON.stringify({ error: 'Invalid user ID' });
}

var gr = new GlideRecord('sys_user');
if (gr.get(userId)) {
return JSON.stringify({ name: gr.getDisplayValue('name') });
}
return JSON.stringify({ error: 'User not found' });
}