Skip to main content
Version: Latest (4.6.0)

Service Catalog

Guide for building ServiceNow Service Catalog components using the Fluent API -- catalog items and record producers. 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

  1. 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.
  2. Variable Naming: Use snake_case for variable names. Use order increments of 100.
  3. Record Producer Tables: Only use for task-based tables (incident, change_request, problem). Never for sc_req_item, sc_request, sc_task.
  4. Field Mapping: Use mapToField: true for simple mappings, scripts for complex logic.
  5. UI Policy vs Client Script: Use UI policies for simple show/hide/mandatory. Use client scripts for validation, calculations, async calls.
  6. onChange Guard: Always start onChange scripts with if (isLoading) return;.
  7. onSubmit: Avoid GlideAjax in onSubmit (async issues). Return false to block submission.
  8. Variable References: Use object references in properties (e.g., catalogItem.variables.urgency), strings inside script code (e.g., g_form.getValue('urgency')).
  9. Variable Sets: Use for reusable variable groups. UI policies and client scripts can be scoped to a variable set with appliesTo: 'set'.
  10. DOM Manipulation: Never manipulate DOM directly -- always use g_form API.
  11. Variable Name Conflicts: Do not use the same variable name as a target table field name.
  12. Record Producer Scripts: Never call current.update() or current.insert() in pre-insert script.
  13. 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: Controlled via user criteria on the catalog item: availableFor grants access, notAvailableFor restricts it. notAvailableFor always overrides availableFor when both are present.

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)

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
  • 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

Properties

PropertyTypeDescription
$idNow.ID[string]Required. Unique identifier.
namestringRequired. Name to appear in the catalog.
shortDescriptionstringBrief summary shown in catalog listings.
descriptionstringDetailed description shown on the item page.
catalogsstring[]sys_ids of existing catalogs.
categoriesstring[]sys_ids of existing categories.
assignedTopicsstring[]sys_ids of existing topics. Controls ESC portal visibility.
accessType'restricted' | 'unrestricted'Controls who can request the item. Default: 'restricted'.
availableForstring[]sys_ids of user criteria for availability.
notAvailableForstring[]sys_ids of user criteria for restrictions (overrides availableFor).
rolesstring[]Roles for catalog item access.
activebooleanWhether the item is active. Default: true.
availability'desktopOnly' | 'both' | 'mobileOnly'Platform availability. Default: 'desktopOnly'.
requestMethod'order' | 'request' | 'submit'Submission button label. Default: 'order'.
flowstringFlow Designer flow for fulfillment (recommended).
workflowstringLegacy workflow for fulfillment.
executionPlanstringDelivery plan for fulfillment.
fulfillmentAutomationLevel'unspecified' | 'manual' | 'semiAutomated' | 'fullyAutomated'Automation level.
fulfillmentGroupstringGroup responsible for delivery.
deliveryTimeDurationEstimated delivery time { days, hours }.
pricingDetailsarrayPricing breakdown: { amount, currencyType, field }.
recurringFrequencystringRequired when pricingDetails contains 'recurring_price'.
variablesobjectVariable definitions for the form.
variableSetsarrayVariable set references: { variableSet, order }.

UI Display Options

PropertyTypeDescription
hideAddToCartbooleanHides "Add to Cart" button
hideAttachmentbooleanHides attachment section
hideDeliveryTimebooleanHides delivery time
hideQuantitySelectorbooleanHides quantity selection
hideSaveAsDraftbooleanHides "Save as Draft"
hideSPbooleanHides from Service Portal
hideAddToWishListbooleanHides "Add to Wishlist"
ignorePricebooleanIgnores price display
omitPricebooleanOmits price entirely
mandatoryAttachmentbooleanRequires attachment
makeItemNonConversationalbooleanPrevents virtual agent ordering
showVariableHelpOnLoadbooleanShows help text by default

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 } 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

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