Skip to main content
Version: 4.8.1

Service Catalog

Guide for building ServiceNow Service Catalog components using the Fluent API -- catalog items and record producers. Visibility is controlled via User Criteria (availableFor/notAvailableFor), never ACLs or the roles property. For variables, variable sets, UI policies, and client scripts, see service-catalog-variables-guide.md. Requires SDK 4.3.0 or higher.

When to Use​

  • Creating catalog items for ordering goods or services
  • Creating record producers for direct task record creation (incidents, changes, problems)
  • Defining catalog variables (form fields) for user input
  • Creating variable sets for reusable variable groups
  • Implementing catalog UI policies for show/hide, mandatory, read-only, and simple value setting
  • Adding catalog client scripts for complex validation, dynamic calculations, API/async calls (GlideAjax), or form submission control (onSubmit)

Instructions​

⚠️ Planning Rule: If the user's requirement mentions restricting who can "see", "view", "access", "order", or "submit" a catalog item — the plan should include a User Criteria step, NOT an ACL step. ACLs are never needed for catalog item visibility. Do not add a "security/ACL" step to the plan for this use case.

  1. Visibility Control: For controlling who can see and order a catalog item, always use availableFor/notAvailableFor with User Criteria records (see user-criteria-guide). Never use ACLs or the roles property -- User Criteria is the only supported mechanism for catalog item visibility. This is self-contained — do not add a separate security/ACL plan step for catalog item visibility.
  2. Catalog, Category & Taxonomy: Items must be assigned to at least one catalog and category, and optionally a taxonomy topic. Use queries to find existing sys_ids.
  3. Variable Naming: Use snake_case for variable names. Use order increments of 100.
  4. Record Producer Tables: Only use for task-based tables (incident, change_request, problem). Never for sc_req_item, sc_request, sc_task.
  5. Field Mapping: Use mapToField: true for simple mappings, scripts for complex logic.
  6. UI Policy vs Client Script: Use UI policies for simple show/hide/mandatory. Use client scripts for validation, calculations, async calls.
  7. onChange Guard: Always start onChange scripts with if (isLoading) return;.
  8. onSubmit: Avoid GlideAjax in onSubmit (async issues). Return false to block submission.
  9. Variable References: Use object references in properties (e.g., catalogItem.variables.urgency), strings inside script code (e.g., g_form.getValue('urgency')).
  10. Variable Sets: Use for reusable variable groups. UI policies and client scripts can be scoped to a variable set with appliesTo: 'set'.
  11. DOM Manipulation: Never manipulate DOM directly -- always use g_form API.
  12. Variable Name Conflicts: Do not use the same variable name as a target table field name.
  13. Record Producer Scripts: Never call current.update() or current.insert() in pre-insert script.
  14. Circular Dependency (Flow + CatalogItem): When a flow uses getCatalogVariables with a catalog item's variables, the flow file imports the CatalogItem, and the CatalogItem references the flow using Now.ref() (NO import) to break the cycle.

Key Concepts​

Catalog Item vs Record Producer​

AspectCatalog ItemRecord Producer
CreatesREQ + RITM + Fulfillment TasksRecord in target table (incident, change_request, etc.)
FulfillmentFlow Designer / Workflow / Delivery PlanServer-side scripts
Use whenOrdering goods/services with approvalsCreating task records directly
Examples"Request Laptop", "Software License""Report Incident", "Submit HR Case"

Key Rule: Ordering/requesting something --> Catalog Item. Creating a task record --> Record Producer.

Taxonomy & Access​

Taxonomy (taxonomy_topic): Hierarchical classification on catalog items. Organizes items from broad categories to specific subcategories, improving searchability and navigation -- particularly in Employee Center, where it maps items to topics and appears above the item name in search results. Assign topics to a catalog item using the assignedTopics property.

Catalog & Category Assignment: Items must belong to at least one Catalog (sc_catalog) and Category (sc_category). Categories can be nested into subcategories. Items can appear in multiple catalogs and categories simultaneously.

Visibility (REQUIRED): Always use user criteria (availableFor/notAvailableFor) to control who can see and order catalog items -- even for simple single-role restrictions. Do not use the roles property for this purpose. notAvailableFor always overrides availableFor when both are present. See the user-criteria-guide for creating criteria records.

User Criteria vs roles Property​

AspectUser Criteria (availableFor)roles Property
Use forCatalog item visibility (who can see/order)Legacy compatibility only
Hides in browsing✅ Yes - fully hides items❌ Partial - items may still appear
SupportsRoles, groups, departments, locations, companies, scriptsRoles only
Reusable✅ Yes - across multiple items❌ No - per item only
Exclusion lists✅ Yes (notAvailableFor)❌ No
Recommended✅ Always use for new implementations❌ Avoid - legacy only

When to use each:

  • User Criteria (availableFor): Always use for controlling who can see and order catalog items. This is the platform-standard mechanism.
  • roles property: Do not use for new implementations. Retained only for backward compatibility with existing items.

UI Policy vs Client Script​

Use CaseUI PolicyClient Script
Show/hide variablesPreferredSupported
Make variables mandatoryPreferredSupported
Make variables read-onlyPreferredSupported
Set variable valuesSupportedSupported
Complex validationLimitedPreferred
Dynamic calculationsLimitedPreferred
API calls / asyncNot supportedSupported
Form submission controlNot supportedSupported

Common Validation Scenarios​

ValidationImplementationScript Type
No past datesClient ScriptonChange
Date range (start < end)Client ScriptonChange
Min/max numeric valuesClient ScriptonChange
Text min/max lengthClient ScriptonSubmit
Format validation (regex)Client ScriptonChange or onSubmit
Required based on another fieldUI Policy (preferred) or Client ScriptonChange
Lookup / async validationClient Script with GlideAjaxonChange

Decision Tree​

  1. Ordering goods/services --> Catalog Item with variables and Flow Designer
  2. Creating task records (incident, change, problem) --> Record Producer with field mapping
  3. Reusable form fields across items --> Variable Set (singleRow or multiRow)
  4. Simple show/hide/mandatory logic --> Catalog UI Policy
  5. Complex validation, calculations, async calls --> Catalog Client Script
  6. Grid/table data entry --> Multi-Row Variable Set (MRVS)
  7. Restricting who can see/order an item --> User Criteria (availableFor/notAvailableFor) via user-criteria-guide

Avoidance​

  • Never use catalog items for creating task records directly (use Record Producers)
  • Never create record producers for sc_request, sc_req_item, sc_task
  • Never call current.update() or current.insert() in pre-insert scripts
  • Never call current.setAbortAction() in Record Producer scripts
  • Never use GlideAjax in onSubmit scripts (async issues)
  • Never manipulate DOM directly -- always use g_form API
  • Never use the same variable name as a target table field name
  • Never skip the order property on variables
  • Never skip catalogs or categories assignment
  • Never hard-code sys_ids without documenting their source
  • Never use the roles property on catalog items for visibility control -- always use availableFor with a User Criteria record instead. The roles property does not fully hide items in catalog browsing; User Criteria is the platform-standard mechanism for catalog visibility.
  • Variables without names cannot be accessed by client scripts
  • Mandatory variables without values cannot be hidden by UI policies
  • Multi-row variable sets have restrictions on certain variable types (no attachments, containers, HTML, macros)
  • Container variables must be properly paired (Start/Split/End)

Catalog Item API Reference​

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

Key properties:

  • $id, name, shortDescription, description — Basic identification
  • catalogs, categories, assignedTopics — Taxonomy and organization
  • availableFor, notAvailableFor — User Criteria for visibility control (see user-criteria-guide)
  • flow, workflow, executionPlan — Fulfillment configuration (mutually exclusive)
  • variables, variableSets — Form fields
  • active, availability, requestMethod — Activation and display settings

Fulfillment Configuration​

Flow Designer (flow) -- Recommended fulfillment method. Use Now.ref() to reference a project-defined flow or provide a sys_id string for an existing platform flow:

// Reference a project-defined flow (avoids circular dependency)
flow: Now.ref("sys_hub_flow", "my_fulfillment_flow");

// Reference an existing flow by sys_id
flow: "e0d08b13c3330100c8b837659bba8fb4";

Pricing Configuration​

Use pricingDetails array with { amount, currencyType, field } objects. Supported field values: price, recurring_price. When using recurring_price, recurringFrequency is required (monthly, yearly, etc.).

Circular Dependency Resolution (Flow + CatalogItem)​

When a flow needs to use getCatalogVariables with the catalog item's variables:

  1. Flow --> imports CatalogItem (can use getCatalogVariables with variables)
  2. CatalogItem --> uses Now.ref() to reference Flow (NO import)
// catalog-item.now.ts - Uses Now.ref(), does NOT import flow
export const myCatalogItem = CatalogItem({
$id: Now.ID["my_catalog_item"],
flow: Now.ref("sys_hub_flow", "my_flow"), // No import needed
variables: { ... }
});

// flow.now.ts - Imports catalog item for getCatalogVariables
import { myCatalogItem } from "../catalog-item.now";

export const myFlow = Flow(
{ $id: Now.ID["my_flow"] },
wfa.trigger(trigger.application.serviceCatalog, ...),
_params => {
const vars = wfa.action(action.core.getCatalogVariables, {
template_catalog_item: `${myCatalogItem}`,
catalog_variables: [myCatalogItem.variables.field1, ...]
});
}
);

Record Producer API Reference​

Properties​

PropertyTypeDescription
$idNow.ID[string]Required. Unique identifier.
tableTableNameRequired. Target table (e.g., 'incident', 'change_request').
namestringRequired. Name to appear in the catalog.
scriptstringServer-side script before record creation.
postInsertScriptstringScript after record creation. Safe to call current.update().
saveScriptstringScript on step save in Catalog Builder.
redirectUrl'generatedRecord' | 'catalogHomePage'Redirect after creation. Default: 'generatedRecord'.
allowEditbooleanAllow editing after creation. Default: false.
canCancelbooleanAllow user to cancel. Default: false.
variablesobjectVariable definitions for the form.
variableSetsarrayVariable set references.

All catalog item properties (catalogs, categories, accessType, etc.) also apply to record producers.

Field Mapping Methods​

ScenarioRecommended Method
Simple text/choice mappingmapToField: true
System values (gs.getUserID())Script
Conditional logicScript
Calculated valuesScript
Variables in Variable SetsScript

Script Types​

ScriptTimingCan call update()?
scriptBefore insertNo
postInsertScriptAfter insertYes
saveScriptOn step saveNo

Available Script Objects​

ObjectDescription
currentGlideRecord of the record being created
producer.var_nameForm variable values
cat_itemRecord Producer definition (postInsertScript only)
gsGlideSystem

Script Rules​

  • Never call current.update() or current.insert() in pre-insert script
  • Never call current.setAbortAction()
  • Never set current.sys_class_name
  • Use postInsertScript for post-creation updates, related records, notifications

Unsupported Tables​

Do not create record producers for sc_request, sc_req_item, sc_task -- use Catalog Items instead.


Examples​

Basic Catalog Item with Variables​

import { CatalogItem, SelectBoxVariable, MultiLineTextVariable } from "@servicenow/sdk/core";

const serviceCatalog = "e0d08b13c3330100c8b837659bba8fb4";
const hardwareCategory = "d258b953c611227a0146101fb1be7c31";
const hardwareTopic = "782413a7c3053010069aec4b7d40ddf1";
const itilUsers = "2f137fb2eb303010e0ef83c45e52287c";
const guestUsers = "76f09af6cb1200108ad442fcf7076dbf";

export const laptopRequest = CatalogItem({
$id: Now.ID["laptop_request"],
name: "Laptop Request",
shortDescription: "Request a new laptop for work",
description: "Submit a request for a new laptop with configuration options.",

catalogs: [serviceCatalog],
categories: [hardwareCategory],
assignedTopics: [hardwareTopic],
availableFor: [itilUsers],
notAvailableFor: [guestUsers],

pricingDetails: [{ amount: 1299, currencyType: "USD", field: "price" }],

variables: {
laptopType: SelectBoxVariable({
question: "Laptop Type",
choices: {
standard: { label: "Standard Laptop", sequence: 1 },
developer: { label: "Developer Workstation", sequence: 2 }
},
mandatory: true,
order: 100
}),
justification: MultiLineTextVariable({
question: "Business Justification",
mandatory: true,
order: 200
})
},

flow: "523da512c611228900811a37c97c2014",
fulfillmentAutomationLevel: "semiAutomated",
deliveryTime: { days: 7, hours: 0 },
accessType: "restricted",
availability: "both",
requestMethod: "order"
});

Catalog Item with Variable Sets and Recurring Pricing​

import { CatalogItem, SingleLineTextVariable, SelectBoxVariable } from "@servicenow/sdk/core";
import { contactInfoSet } from './variable-sets/contact-info-set';
import { approvalInfoSet } from './variable-sets/approval-info-set';

const serviceCatalog = "e0d08b13c3330100c8b837659bba8fb4";
const softwareCategory = "d258b953c611227a0146101fb1be7c31";

export const softwareLicenseRequest = CatalogItem({
$id: Now.ID["software_license_request"],
name: "Software License Request",
shortDescription: "Request a software license",

catalogs: [serviceCatalog],
categories: [softwareCategory],

variableSets: [
{ variableSet: contactInfoSet, order: 100 },
{ variableSet: approvalInfoSet, order: 200 }
],

variables: {
software_name: SingleLineTextVariable({
question: "Software Name",
mandatory: true,
order: 100
}),
license_type: SelectBoxVariable({
question: "License Type",
choices: {
individual: { label: "Individual", sequence: 1 },
team: { label: "Team (5 seats)", sequence: 2 },
enterprise: { label: "Enterprise (unlimited)", sequence: 3 }
},
mandatory: true,
order: 200
})
},

pricingDetails: [
{ amount: 0, currencyType: "USD", field: "price" },
{ amount: 99, currencyType: "USD", field: "recurring_price" }
],
recurringFrequency: "monthly",

flow: "523da512c611228900811a37c97c2014",
deliveryTime: { days: 3, hours: 0 }
});

Record Producer with Field Mapping​

import { CatalogItemRecordProducer, SingleLineTextVariable, SelectBoxVariable, ReferenceVariable } from "@servicenow/sdk/core";
import { rpPreInsert } from "../../modules/record-producers/rp-pre-insert";
import { rpPostInsert } from "../../modules/record-producers/rp-post-insert";

const serviceCatalog = "e0d08b13c3330100c8b837659bba8fb4";
const itServicesCategory = "d258b953c611227a0146101fb1be7c31";

export const incidentProducer = CatalogItemRecordProducer({
$id: Now.ID["comprehensive_incident_producer"],
name: "Report Incident with Full Configuration",
shortDescription: "Complete incident producer with variables and scripts",
table: "incident",

catalogs: [serviceCatalog],
categories: [itServicesCategory],

variables: {
short_description: SingleLineTextVariable({
question: "Brief Summary",
mandatory: true,
mapToField: true,
field: "short_description",
order: 100
}),
urgency: SelectBoxVariable({
question: "Urgency",
mandatory: true,
mapToField: true,
field: "urgency",
choices: {
"1": { label: "High", sequence: 1 },
"2": { label: "Medium", sequence: 2 },
"3": { label: "Low", sequence: 3 }
},
order: 200
}),
assignment_group: ReferenceVariable({
question: "Assignment Group",
mapToField: true,
field: "assignment_group",
referenceTable: "sys_user_group",
order: 300
})
},

script: rpPreInsert,
postInsertScript: rpPostInsert,
redirectUrl: "generatedRecord",
view: "ess",
allowEdit: true
});

modules/record-producers/rp-pre-insert.js:

import { gs } from '@servicenow/glide'

export function rpPreInsert(current, producer) {
current.impact = 3;
current.contact_type = "self-service";
current.caller_id = gs.getUserID();

if (producer.urgency === "1") {
current.priority = 1;
current.assignment_group = "Hardware Team";
}
// Do NOT use current.update() or current.insert() here
}

modules/record-producers/rp-post-insert.js:

import { gs, GlideRecord } from '@servicenow/glide'

export function rpPostInsert(current, producer) {
current.work_notes = "Created via Service Catalog at " + gs.nowDateTime();
current.update();

var task = new GlideRecord("sc_task");
task.initialize();
task.request = current.sys_id;
task.short_description = "Follow up on incident: " + current.short_description;
task.insert();
}