logo

NJP

M2M table in catalog builder

New article articles in ServiceNow Community · Sep 02, 2025 · article

M2M table in catalog builder (Wizard)

There’s a helpful article on the ServiceNow Community about this, but here’s a quick summary using the Service Offering example:

  • When a Service Offering is selected, the related list Available for Subscribers on the catalog item should be populated.
  • This behavior can vary based on requirements like User Criteria.

To achieve this:

  1. Create two Catalog Wizard Questions:
    • One for selecting the Service Offering.
    • One for populating the related list.
  2. Mapping Difference:
    • The first question should have map to set = false.
    • The second should have map to set = true.
  3. Technical Setup:
    • The first field must be a List Collector.
    • It should be placed on a Variable Set.
    • Ensure the OOTB Catalog OnChange client script is active on the Variable Set—this is essential for populating the related list dynamically.

function onChange(control, oldValue, newValue, isLoading) {

var CATALOG_QUESTION_NAME =Service offering variable name;

var CATALOG_PRODUCER_NAME = M2M producer set name ;

var CATALOG_PRODUCER_QUESTION_NAME = 'service_offering';

if (isLoading) {

g_form.setDisplay(CATALOG_PRODUCER_NAME, false);

convertRowValuesToCommaSeparatedString(CATALOG_QUESTION_NAME, CATALOG_PRODUCER_NAME, CATALOG_PRODUCER_QUESTION_NAME);

return;

}

var valueArr = !newValue ? [] : newValue.split(',');

addRows(CATALOG_PRODUCER_NAME, CATALOG_PRODUCER_QUESTION_NAME, valueArr);

}

//TODO: move to UI scripts and use it in here.

function addRows(producerSetName, producerSetQuestionName, includedValues) {

var recordMap = getRecordMap(producerSetName, producerSetQuestionName);

var ans = [];

for (var i = 0; i < includedValues.length; i++) {

var row = {};

row[producerSetQuestionName] = includedValues[i];

if (recordMap && recordMap[includedValues[i]])

row['sys_id'] = recordMap[includedValues[i]];

ans.push(row);

}

g_form.setValue(producerSetName, JSON.stringify(ans));

}

function convertRowValuesToCommaSeparatedString(questionName, producerSetName, producerQuestionName) {

var value = g_form.getValue(producerSetName);

try {

value = JSON.parse(value);

} catch (e) {

value = [];

}

var ans = [];

for (var i = 0; i < value.length; i++) {

var row = value[i];

if (row && row.hasOwnProperty(producerQuestionName))

ans.push(row[producerQuestionName]);

}

g_form.setValue(questionName, ans.join());

}

function getRecordMap(producerSetName, producerQuestionName) {

var fieldObj = g_form.$private.getField(producerSetName);

if (fieldObj._recordMap)

return fieldObj._recordMap;

var originalValue = fieldObj.originalValue || '[]';

var originalJSON;

try {

originalJSON = JSON.parse(originalValue);

} catch (e) {

originalJSON = [];

}

var recordMap = {};

for (var i = 0; i < originalJSON.length; i++) {

var obj = originalJSON[i];

if (obj && obj[producerQuestionName] && obj['sys_id'])

recordMap[obj[producerQuestionName]] = obj['sys_id'];

}

fieldObj._recordMap = recordMap;

return fieldObj._recordMap;

}

This is a major step that isn’t documented. Everything else follows the out-of-the-box behavior—try to replicate it similar to how "Available for" (User Criteria ) works.

Thanks
Amarjeet Pal

View original source

https://www.servicenow.com/community/developer-blog/m2m-table-in-catalog-builder/ba-p/3368345