Skip to main content
Version: 4.8.0

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();
}