@servicenow/sdk
Version:
ServiceNow SDK
993 lines (797 loc) • 32.8 kB
Markdown
---
tags: [catalog variable, variable set, catalog ui policy, catalog client script, item_option_new, item_option_new_set, catalog_ui_policy, catalog_script_client, onChange, onLoad, onSubmit, g_form, dynamic fields]
---
# Service Catalog Variables
API reference and patterns for Service Catalog variables, variable sets, UI policies, and client scripts. For catalog items and record producers, see [service-catalog-guide.md](service-catalog-guide.md). Requires SDK 4.3.0 or higher.
---
## Catalog Variables API Reference
### Common Variable Properties
| Property | Type | Description |
|---|---|---|
| `question` | string | **Required.** Label text displayed to user. |
| `order` | number | Display order (use increments of 100). |
| `mandatory` | boolean | Whether field is required. Default: `false`. |
| `readOnly` | boolean | Whether field is editable. Default: `false`. |
| `hidden` | boolean | Whether field is visible. Default: `false`. |
| `tooltip` | string | Hover help text. |
| `exampleText` | string | Placeholder text. |
| `instructions` | string | Inline help text. |
| `defaultValue` | string | Pre-filled value. |
| `width` | 25 \| 50 \| 75 \| 100 | Field width percentage. |
| `readRoles` | string[] | Roles that can read the variable. |
| `writeRoles` | string[] | Roles that can write to the variable. |
| `mapToField` | boolean | Map to target table field (Record Producers). |
| `field` | string | Target field name when mapToField is true. |
### Variable Types
**Text Variables**
- **SingleLineTextVariable** -- Single line text input
- **MultiLineTextVariable** -- Multi-line text area
- **WideSingleLineTextVariable** -- Full-width single line
- **EmailVariable** -- Email address input
- **UrlVariable** -- URL input
- **IpAddressVariable** -- IPv4/IPv6 input
- **MaskedVariable** -- Masked/password input (supports `useEncryption`, `useConfirmation`)
**Choice Variables**
- **SelectBoxVariable** -- Dropdown choice list. Requires `choices` object with `{ label, sequence }`.
- **MultipleChoiceVariable** -- Radio buttons. Supports `choiceDirection: 'down'` or `'across'`.
- **YesNoVariable** -- Yes/No choice list.
- **CheckboxVariable** -- Checkbox. Use `selectionRequired: true` for mandatory.
- **NumericScaleVariable** -- Likert scale radio buttons.
**Lookup Variables**
- **LookupSelectBoxVariable** -- Dropdown from table data.
- **LookupMultipleChoiceVariable** -- Radio buttons from table data.
**Reference Variables**
- **ReferenceVariable** -- References a record in another table. Key properties: `referenceTable`, `referenceQualCondition`, `useReferenceQualifier`.
- **RequestedForVariable** -- Specifies who the request is for.
- **ListCollectorVariable** -- Select multiple records from a table.
**Date/Time Variables**
- **DateVariable** -- Date picker.
- **DateTimeVariable** -- Date and time picker.
- **DurationVariable** -- Duration input.
**Layout Variables**
- **ContainerStartVariable** / **ContainerSplitVariable** / **ContainerEndVariable** -- Multi-column layout containers. Must be properly paired.
- **LabelVariable** -- Display-only label.
- **BreakVariable** -- Horizontal line separator.
**Special Variables**
- **AttachmentVariable** -- File upload.
- **HtmlVariable** -- Rich content display.
- **RichTextLabelVariable** -- Formatted label.
- **CustomVariable** / **CustomWithLabelVariable** -- UI macro insertion.
- **UIPageVariable** -- UI page insertion.
---
## Variable Set API Reference
### Properties
| Property | Type | Description |
|---|---|---|
| `$id` | Now.ID[string] | **Required.** Unique identifier. |
| `title` | string | **Required.** Display title. |
| `internalName` | string | Internal name. Auto-generated from title if not provided. |
| `description` | string | Description of the variable set. |
| `type` | `'singleRow'` \| `'multiRow'` | Default: `'singleRow'`. |
| `layout` | `'normal'` \| `'2down'` \| `'2across'` | Default: `'normal'`. |
| `order` | number | Display order. Default: `100`. |
| `displayTitle` | boolean | Show collapsible section header. Default: `false`. |
| `setAttributes` | string | Additional config (e.g., `"max_rows=10,collapsible=true"`). |
| `readRoles` | string[] | Roles that can view the variable set. |
| `writeRoles` | string[] | Roles that can modify values. |
| `createRoles` | string[] | Roles that can create instances (for multiRow). |
| `variables` | object | Variable definitions for the set. |
### Attaching to Catalog Items
Attach variable sets via `variableSets: [{ variableSet, order }]` on a Catalog Item or Record Producer. Item-specific variables can be added alongside variable sets.
### Multi-Row Variable Set (MRVS)
Use `type: "multiRow"` for grid/table data entry (e.g., multiple team members). Configure with `setAttributes` for row limits and collapsibility.
### MRVS Unsupported Variable Types
- AttachmentVariable
- ContainerStartVariable / ContainerEndVariable / ContainerSplitVariable
- HtmlVariable
- CustomVariable / CustomWithLabelVariable
- RichTextLabelVariable
- UIPageVariable
### MRVS Limitations
- "Assign to Field" not supported
- Cannot add variables with read roles
- Set row limits using `max_rows` attribute
- Will not display if added to a container
### Role-Based Access
- `readRoles`: Roles that can view the variable set
- `writeRoles`: Roles that can modify values
- `createRoles`: Roles that can create instances (multiRow)
Set-level permissions override variable-level permissions when access is denied at the set level.
---
## Catalog UI Policy API Reference
### Properties
| Property | Type | Description |
|---|---|---|
| `$id` | Now.ID[string] | **Required.** Unique identifier. |
| `shortDescription` | string | **Required.** Description of what the policy does. |
| `catalogItem` | ref | **Required** if not using variableSet. |
| `variableSet` | ref | **Required** if not using catalogItem. |
| `appliesTo` | `'item'` \| `'set'` | Required if using variableSet. Default: `'item'`. |
| `active` | boolean | Whether the policy is active. Default: `true`. |
| `onLoad` | boolean | Run on form load. Default: `true`. |
| `reverseIfFalse` | boolean | Reverse actions when condition is false. Default: `true`. |
| `catalogCondition` | string | Condition using encoded query syntax. |
| `appliesOnCatalogItemView` | boolean | Applies to catalog item view. Default: `true`. |
| `appliesOnTargetRecord` | boolean | Applies to target record. Default: `false`. |
| `appliesOnCatalogTasks` | boolean | Applies to catalog tasks. Default: `false`. |
| `appliesOnRequestedItems` | boolean | Applies to requested items. Default: `false`. |
| `runScripts` | boolean | Execute client scripts. Default: `false`. |
| `executeIfTrue` | string | Script when condition is true. |
| `executeIfFalse` | string | Script when condition is false. |
| `runScriptsInUiType` | `'desktop'` \| `'mobileOrServicePortal'` \| `'all'` | Default: `'desktop'`. |
| `actions` | array | List of variable actions. |
### Action Properties
| Property | Type | Description |
|---|---|---|
| `variableName` | ref | **Required.** Variable reference. |
| `visible` | boolean | Show/hide the variable. |
| `mandatory` | boolean | Make variable required. |
| `readOnly` | boolean | Make variable read-only. |
| `value` | string | Value to set. |
| `valueAction` | `'clearValue'` \| `'setValue'` | How to apply the value. |
| `order` | number | Execution order. Default: `100`. |
| `variableMessage` | string | Message to display on the field. |
| `variableMessageType` | `'info'` \| `'warning'` \| `'error'` | Message severity. |
### Condition Syntax
```typescript fluent
// Simple condition
catalogCondition: `${catalogItem.variables.priority}=high^EQ`;
// Multiple conditions with AND
catalogCondition: `${catalogItem.variables.env}=prod^${catalogItem.variables.critical}=true^EQ`;
// Multiple conditions with OR
catalogCondition: `${catalogItem.variables.env}=prod^OR${catalogItem.variables.critical}=true^EQ`;
// Not empty check
catalogCondition: `${catalogItem.variables.reference}ISNOTEMPTY^EQ`;
```
### Priority Rules
1. **Mandatory** has highest priority
2. If a variable is mandatory and has no value, readonly/hide actions **do not work**
3. If a variable set/container has a mandatory variable without value, the entire set **cannot be hidden**
4. "Clear value" action does not work on variable sets and containers
### Variable Type Limitations
| Policy Type | Not Applicable To |
|---|---|
| Mandatory | Fraction, Container Split, Container End, UI Macro, Label, UI Page |
| Read-only | Fraction, Container Split, Container End, UI Macro, Label, UI Page |
| Visibility | Fraction, Container Split, Container End |
### Policy with Client Scripts
Set `runScripts: true` and provide `executeIfTrue` / `executeIfFalse` scripts via `Now.include(...)`. These scripts run client-side in the browser where modules are not available, so `Now.include()` is the correct approach. Scripts must be wrapped in `function onCondition() {}`.
---
## Catalog Client Script API Reference
### Properties
| Property | Type | Description |
|---|---|---|
| `$id` | Now.ID[string] | **Required.** Unique identifier. |
| `name` | string | **Required.** Name of the script. |
| `script` | string | Inline script or `Now.include()` reference. These are client-side scripts — modules are not available. |
| `type` | `'onLoad'` \| `'onChange'` \| `'onSubmit'` | Script trigger type. |
| `uiType` | `'desktop'` \| `'mobileOrServicePortal'` \| `'all'` | Default: `'desktop'`. |
| `active` | boolean | Whether the script is enabled. Default: `true`. |
| `catalogItem` | ref | **Required** if not using variableSet. |
| `variableSet` | ref | **Required** if not using catalogItem. |
| `appliesTo` | `'item'` \| `'set'` | Required if using variableSet. Default: `'item'`. |
| `variableName` | ref | **Required** for onChange. The variable that triggers the script. |
| `appliesOnCatalogItemView` | boolean | Applies on catalog item view. Default: `true`. |
| `appliesOnRequestedItems` | boolean | Applies on requested items. Default: `false`. |
| `appliesOnCatalogTasks` | boolean | Applies on catalog tasks. Default: `false`. |
| `appliesOnTargetRecord` | boolean | Applies on target record. Default: `false`. |
### 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 will not complete before the form submits.
### g_form API Reference
| Method | Description |
|---|---|
| `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.
### Catalog Client Script vs Standard Client Script
| Aspect | Catalog Client Script | Standard Client Script |
|---|---|---|
| Scope | Catalog item or variable set | Table (e.g., Incident) |
| onChange target | Links to a **variable** | Links to a **field** |
| Context | Catalog ordering, RITM, Catalog Task forms | Table forms |
| Variable access | Direct by variable name | Use `variables.variable_name` prefix |
| Applies to | `item` or `set` | Specific table |
### 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.
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.
### 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.
**Method comparison:**
| Method | Execution | Use When | Avoid When |
|---|---|---|---|
| `getXMLAnswer()` | **Async** | Simple lookups, returning a single value/string | You need the full XML response object |
| `getXML()` | **Async** | Need full XML response, complex response parsing | Simple value returns (use getXMLAnswer) |
| `getXMLWait()` | **Sync** | Almost never -- legacy/global scope only | Scoped apps, any production code |
**Parameter rules:** All custom parameters must start with `sysparm_`. The first `addParam` call must always be `sysparm_name` with the method name.
```javascript
ga.addParam("sysparm_name", "methodName"); // REQUIRED: always first
ga.addParam("sysparm_user_id", userSysId); // 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**.
| Property | Value |
|---|---|
| Name | Must match the string in `new GlideAjax('ClassName')` |
| Client callable | **Checked** (required for GlideAjax access) |
| Extends | `global.AbstractAjaxProcessor` |
| Retrieve params | Use `this.getParameter('sysparm_param_name')` |
| Return data | Use `return` (simple string) or `return JSON.stringify(obj)` for objects |
**Security:** Client callable Script Includes run in the logged-in user's session context. ACLs still apply to GlideRecord queries. Always validate parameters from `this.getParameter()`. Never trust client-side input.
---
## Examples
### Variable Examples
**Text variables:**
```typescript fluent
import { SingleLineTextVariable, MultiLineTextVariable, EmailVariable, MaskedVariable } from '@servicenow/sdk/core'
SingleLineTextVariable({ question: "Employee Name", order: 100, mandatory: true, exampleText: "John Smith" });
MultiLineTextVariable({ question: "Description", order: 200, mandatory: true, width: 100 });
EmailVariable({ question: "Email Address", order: 300, mandatory: true });
MaskedVariable({ question: "Enter Password", order: 400, useEncryption: true });
```
**Choice variables:**
```typescript fluent
import { SelectBoxVariable, MultipleChoiceVariable, CheckboxVariable } from '@servicenow/sdk/core'
SelectBoxVariable({
question: "Priority Level",
order: 100,
choices: {
high: { label: "High", sequence: 1 },
medium: { label: "Medium", sequence: 2 },
low: { label: "Low", sequence: 3 }
},
includeNone: true
});
MultipleChoiceVariable({
question: "Services Required",
choiceDirection: "down",
choices: {
install: { label: "Installation", sequence: 1 },
config: { label: "Configuration", sequence: 2 },
training: { label: "Training", sequence: 3 }
},
order: 200
});
CheckboxVariable({ question: "I agree to the terms", order: 400, selectionRequired: true });
```
**Reference variables:**
```typescript fluent
import { ReferenceVariable, ListCollectorVariable } from '@servicenow/sdk/core'
ReferenceVariable({
question: "Point of Contact",
referenceTable: "sys_user",
referenceQualCondition: "active=true",
order: 100
});
ListCollectorVariable({
question: "Team Members",
listTable: "sys_user",
referenceQual: "active=true",
order: 300,
mandatory: true
});
```
**Container layout (multi-column):**
```typescript fluent
import { ContainerStartVariable, ContainerSplitVariable, ContainerEndVariable, SingleLineTextVariable, EmailVariable } from '@servicenow/sdk/core'
const variables = {
contact_container_start: ContainerStartVariable({
question: "Contact Information",
layout: "2across",
displayTitle: true,
order: 100
}),
first_name: SingleLineTextVariable({
question: "First Name",
mandatory: true,
order: 110
}),
contact_split: ContainerSplitVariable({ order: 200 }),
email: EmailVariable({
question: "Email Address",
mandatory: true,
order: 210
}),
contact_container_end: ContainerEndVariable({ order: 300 })
}
```
**Variables with pricing:**
```typescript fluent
import { CheckboxVariable, SelectBoxVariable } from '@servicenow/sdk/core'
premiumSupport: CheckboxVariable({
question: "Premium Support (+$150)",
pricingDetails: [
{ amount: 150, currencyType: "USD", field: "price_if_checked" },
{ amount: 30, currencyType: "USD", field: "rec_price_if_checked" }
],
order: 500
});
hardwareType: SelectBoxVariable({
question: "Hardware Type",
choices: {
laptop: {
label: "Business Laptop (Base)",
sequence: 1,
pricingDetails: [{ amount: 0, currencyType: "USD", field: "misc" }]
},
workstation: {
label: "Developer Workstation (+$800)",
sequence: 2,
pricingDetails: [{ amount: 800, currencyType: "USD", field: "misc" }]
}
},
mandatory: true,
order: 600
});
```
### Single-Row Variable Set
```typescript fluent
import { VariableSet, EmailVariable, SingleLineTextVariable, ReferenceVariable } from "@servicenow/sdk/core";
export const contactInfoSet = VariableSet({
$id: Now.ID["contact_info_set"],
title: "Contact Information",
description: "Standard contact information fields",
type: "singleRow",
layout: "2across",
order: 100,
displayTitle: true,
variables: {
email: EmailVariable({ question: "Email Address", mandatory: true, order: 100 }),
phone: SingleLineTextVariable({ question: "Phone Number", mandatory: true, order: 200 }),
department: ReferenceVariable({
question: "Department",
referenceTable: "cmn_department",
referenceQualCondition: "active=true",
order: 300
})
}
});
```
### Multi-Row Variable Set (MRVS)
```typescript fluent
import { VariableSet, ReferenceVariable, SelectBoxVariable, DateVariable } from '@servicenow/sdk/core'
export const teamMembersSet = VariableSet({
$id: Now.ID["team_members_set"],
title: "Team Members",
description: "Add multiple team members who need access",
type: "multiRow",
layout: "2across",
displayTitle: true,
setAttributes: "max_rows=10,collapsible=true",
readRoles: ["admin", "manager"],
writeRoles: ["admin"],
variables: {
user: ReferenceVariable({
question: "User",
referenceTable: "sys_user",
referenceQualCondition: "active=true",
mandatory: true,
order: 100
}),
accessLevel: SelectBoxVariable({
question: "Access Level",
choices: {
read: { label: "Read Only", sequence: 1 },
write: { label: "Write", sequence: 2 },
admin: { label: "Admin", sequence: 3 }
},
mandatory: true,
order: 200
}),
startDate: DateVariable({ question: "Access Start Date", mandatory: true, order: 300 })
}
});
```
### Attaching Variable Sets to a Catalog Item
```typescript fluent
import { CatalogItem, MultiLineTextVariable } from '@servicenow/sdk/core'
import { contactInfoSet } from './variable-sets/contact-info-set'
import { teamMembersSet } from './variable-sets/team-members-set'
import { serviceCatalog } from './catalogs/service-catalog'
import { itServicesCategory } from './categories/it-services'
export const accessRequest = CatalogItem({
$id: Now.ID["access_request"],
name: "Team Access Request",
shortDescription: "Request access for team members",
catalogs: [serviceCatalog],
categories: [itServicesCategory],
variableSets: [
{ variableSet: contactInfoSet, order: 100 },
{ variableSet: teamMembersSet, order: 200 }
],
variables: {
notes: MultiLineTextVariable({ question: "Additional Notes", order: 100 })
},
flow: "523da512c611228900811a37c97c2014"
});
```
### Catalog UI Policy -- Show/Hide Based on Condition
```typescript fluent
import { CatalogUiPolicy } from "@servicenow/sdk/core";
import { hardwareRequestItem } from "./catalog-items/HardwareRequest.now";
export const managerApprovalPolicy = CatalogUiPolicy({
$id: Now.ID["manager_approval_policy"],
shortDescription: "Show manager approval when high priority selected",
catalogItem: hardwareRequestItem,
catalogCondition: `${hardwareRequestItem.variables.priority}=high^EQ`,
actions: [
{
variableName: hardwareRequestItem.variables.manager_approval,
visible: true,
mandatory: true
}
]
});
```
### Catalog UI Policy -- Read-Only with Value
```typescript fluent
import { CatalogUiPolicy } from "@servicenow/sdk/core"
import { softwareRequestItem } from './catalog-items/software-request'
export const readOnlyPolicy = CatalogUiPolicy({
$id: Now.ID["readonly_license_policy"],
shortDescription: "Make license type read-only for standard software",
catalogItem: softwareRequestItem,
catalogCondition: `${softwareRequestItem.variables.software_type}=standard^EQ`,
actions: [
{
variableName: softwareRequestItem.variables.license_type,
readOnly: true,
value: "standard_license",
valueAction: "setValue"
}
]
});
```
### Catalog UI Policy -- With Client Scripts
```typescript fluent
import { CatalogUiPolicy } from "@servicenow/sdk/core"
import { cloudVmRequest } from './catalog-items/cloud-vm-request'
CatalogUiPolicy({
$id: Now.ID["vm_prod_controls_policy"],
shortDescription: "VM: Prod/BizCritical Controls",
catalogItem: cloudVmRequest,
catalogCondition: `${cloudVmRequest.variables.environment}=prod^OR${cloudVmRequest.variables.business_critical}=true^EQ`,
active: true,
onLoad: true,
reverseIfFalse: true,
runScripts: true,
runScriptsInUiType: "all",
actions: [
{
variableName: cloudVmRequest.variables.backup_required,
value: "true",
valueAction: "setValue",
readOnly: true,
order: 100
},
{
variableName: cloudVmRequest.variables.cost_center,
mandatory: true,
order: 200
}
],
executeIfTrue: Now.include("../../scripts/vm-production-controls.js"),
executeIfFalse: Now.include("../../scripts/vm-development-controls.js")
});
```
**vm-production-controls.js:**
```javascript
function onCondition() {
var PROD_REGIONS = [
["AP-South-1", "AP-South-1 (Mumbai)"],
["EU-West-1", "EU-West-1 (Ireland)"]
];
g_form.clearOptions("region");
PROD_REGIONS.forEach(function (pair) {
g_form.addOption("region", pair[0], pair[1]);
});
g_form.showFieldMsg(
"environment",
"Production VMs enforce backup and require cost center.",
"info"
);
}
```
### Catalog UI Policy -- Applied to Variable Set
```typescript fluent
import { CatalogUiPolicy } from "@servicenow/sdk/core"
import { shippingVariableSet } from './variable-sets/shipping'
export const internationalShippingPolicy = CatalogUiPolicy({
$id: Now.ID["international_shipping_policy"],
shortDescription: "Show customs fields for international shipping",
variableSet: shippingVariableSet,
appliesTo: "set",
catalogCondition: `${shippingVariableSet.variables.shipping_country}!=US^EQ`,
appliesOnCatalogItemView: true,
appliesOnRequestedItems: true,
actions: [
{
variableName: shippingVariableSet.variables.customs_declaration,
visible: true,
mandatory: true,
variableMessage: "Required for international shipping",
variableMessageType: "warning"
}
]
});
```
### Catalog Client Script -- onLoad
```typescript fluent
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:**
```javascript
function onLoad() {
g_form.setReadOnly("estimated_cost", true);
g_form.setValue("estimated_cost", "$0");
g_form.setMandatory("justification", true);
}
```
### Catalog Client Script -- onChange
```typescript fluent
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:**
```javascript
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
```typescript fluent
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:**
```javascript
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
```typescript fluent
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:**
```javascript
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
```typescript fluent
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,
uiType: "all"
});
```
**department-change.js:**
```javascript
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):**
```javascript
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):**
```javascript
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):**
```javascript
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):**
```javascript
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
```javascript
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
```javascript
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' });
}
```