Laszlo Balla
ServiceNow Employee
ServiceNow Employee

Table of Contents

 

TL;DR: GitHub

In case you’re just here to take some code snippets with basic explanation, head over to the GitHub repository maintained by the ServiceNow Developer Program and the community: GitHub link.

 

Concept

Decision Tables offer a very powerful, yet easily maintainable way to drive workflow automation logic in ServiceNow. The purpose of this article however is not to explain how to use them - it was done by someone more competent than me, @Lisa Holenstein: check and bookmark this article in the Workflow Automation CoE that links to some awesome posts on this topic!

 

Background

A few weeks ago I had a requirement to add a new Selectbox variable to a bunch of catalog items, and show different choice options depending on which item this variable is displayed on. In case of some items, we would have to look at the value of another variable as well to determine the actual choices. I needed a solution that can easily scale to additional items in the future, and can be maintained by a less experienced administrator, or even a process user directly. I immediately thought of Decision Tables, but wasn’t sure that it would actually work out as I hoped. It did, and here are the steps I took.

 

Prerequisites

First of all, you will need a Decision Table with a result column of type 'Choice', and at least one, mandatory input that is a reference to sc_cat_item.

You will have to define the decisions for each catalog item separately, and obviously one catalog item can have multiple results - this is how you will get multiple choices for your Selectbox variable at the end. If you have other inputs, i.e. for different values from other variables, you simply need to add those conditions for each decision line for the relevant catalog item.

⚠️ Note: if you created the choices inside the Decision Table, make sure the values are not too long. They end up getting truncated in the sys_decision_answer table, and sebsequently the values stored in sys_choice will not match. So if necessary, change the default value to something short and unique (or use an existing choice list if you can).

 

If it  sounds too complicated, bear with me, I will show you below.

 

Besides the Decision Table, you will need (at least) 2 scripts:

  • A client callable Script Include to which you will pass the catalog item sys_id and any variable values your decision table may need. The Script Include will use the Decision Table API to get all defined choices for the catalog item and return a stringified array of objects with the choice values and labels.
  • An onLoad (if you only want to consider the catalog item as an input) or an onChange (if you need to catch values from other variables too) catalog client script that will call the script include, and add the options (choices) to the target variable based on the returned results, using GlideAjax.

 

And finally, you will of course need the variable itself, of type Selectbox.

 

Decision Table

In every decision table, we focus on 3 main elements: inputs, conditions, results. Let’s look through them one by one.

 

Inputs

We have one mandatory input, which is a reference to the actual catalog item our variable is displayed on. If you want to evaluate values from other variables on the catalog form as well, add an additional input for that, either as one generic string field, or if you prefer, dedicated ones for each specific variable (in which case, make sure they are not mandatory).

Here is what my example looks like:

LaszloBalla_0-1708892385005.png

 

 

Conditions

This is fairly simple. Add a condition column for the Catalog Item input you added above:

LaszloBalla_1-1708892385049.png

 

If you have more inputs, then add columns for each of those as well, e.g.:

LaszloBalla_2-1708892385429.png

 

Results

The result column must be of type Choice. I have defined new choices for my use case, but you should be able to use an existing choice list too. If you have long strings as your choice labels, make sure you update the choice value manually to something reasonably short. I am not sure what the actual character limit is (leave a comment if you do!), but I had problems with values getting truncated and not matching with the sys_choice values (on a Utah instance). You will want to have something like this:

LaszloBalla_3-1708892385108.png

 

 

Decision Rows

Now that all main elements are configured, it’s time to populate our table with data. You will select the required catalog item in your condition column, and choose a (single) choice for that item. If you need more choices as options for your target Selectbox variable, you need to add multiple rows for the same item. The DecisionTableApi will return each matching result for that item.

💡Hint: since the Washington DC release, you have the ability to duplicate decision rows! (Credits to @Bimschleger for pointing this out for me!)

 

Here is an example with some demo data, with 2 condition columns:

LaszloBalla_4-1708892385052.png

 

Script Include

As mentioned above, the script include must be client callable, and it will:

  1. Take the inputs from a catalog client script
  2. Pass the inputs to the DecisionTableAPI
  3. Use the results from the DecisionTableAPI to look-up the actual choice records from the sys_choice table
  4. Construct an array of objects (with choice labels and values) to be returned to the client script

 

💡Hint: using the DecisionTableAPI in scripts became much easier with the Washington release, as you can create code snippets right from the the Decision Builder (link).

 

Furthermore, you will need to collect some technical values from your decision table, and make it available to your script include, either by hardcoding them, storing them in system properties, other decision tables(!), etc.

 

  • Decision Table sys_id {var decisionTableId}: you can get it from the sys_decision table
  • Technical names of your decision inputs {var dtInput1, dtInput2, etc.}: you can get these from the sys_decision_input table. Filter for: Table ends with [Decision Table sys_id from above].
    Note that these will always start with u_ for your decision tables, e.g. u_catalog_item.
  • The technical name of the result column that has our choices {var resultColumn}: you can get this from the sys_decision_multi_result_element table. The same things apply as above, both for how to filter and that the column name will start with u_.

Example

Here’s an example script include to get you started with the above variables left empty:

 

 

var GetChoicesFromDT = Class.create();
GetChoicesFromDT.prototype = Object.extendsObject(global.AbstractAjaxProcessor, {

    getChoices: function() {
	    
         /**
	 * Gets the defined choices for the passed in catalog item
  	 * 
    	 * @author Laszlo Balla
	 * @param {String} sysparm_cat_item
  	 *    The sys_id of the catalog item to get choices for - mandatory
    	 * @param {String} sysparm_cat_variable
      	 *    Value from an additional catalog variable to evaluate as part of your decision - optional
	 * @return {String}
  	 *    A stringified array (since it goes to client script) of choices
  	 */
	
	
	 /**
         * In addition to the above, the following variable MUST be set for the script to work:
	 *
	 ** decisionTableId : Sys ID of the decision table. Store in a system property and set with gs.getProperty()
         ** dtInput1, 2, etc. :  the technical names of the Decision Table inputs
	 ** resultColumn : the technical name of the result column of your Decision Table that has the choices set
         */

        var catItem = gs.nil(this.getParameter('sysparm_cat_item')) ? null : this.getParameter('sysparm_cat_item'); // Mandatory parameter
        var catVar = gs.nil(this.getParameter('sysparm_cat_variable')) ? null : this.getParameter('sysparm_cat_variable'); // Optional parameter example (variable from record producer). Multiple as needed, or remove if not.
        var decisionTableId = ''; //Sys ID of the decision table. Store in a system property and set with gs.getProperty()
        var dtInput1 = ''; // Make sure you set this to the technical name of the first input of your Decision Table
        var dtInput2 = ''; // Make sure you set this to the technical name of the second input of your Decision Table, if you have one. Multiply as needed, or remove if not.
        var resultColumn = ''; // Set this to the technical name of the result column that contains your choices
        var answerArray = [];
        var choiceArr = [];
        var iter1 = 0;

        if (!gs.nil(catItem) && !gs.nil(decisionTableId)) {
            var choiceQuery = 'var__m_sys_decision_multi_result_element_' + decisionTableId;
            var decisonTable = new sn_dt.DecisionTableAPI();
            var inputs = new Object();
            inputs[dtInput1] = '' + catItem;

            // Repeat this block as necessary with additional parameters and inputs
            if (!gs.nil(catVar)) {
                inputs[dtInput2] = '' + catVar;
            }

            var dtResponse = decisonTable.getDecisions(decisionTableId, inputs);
            while (iter1 < dtResponse.length) {
                answerArray.push(dtResponse[iter1]['result_elements'][resultColumn].toString());
                iter1++;
            }
	   // Now find the the actual choices with labels
            var choiceGr = new GlideRecord('sys_choice');
            choiceGr.addQuery('name', choiceQuery);
            choiceGr.addQuery('value', 'IN', answerArray.toString());
            choiceGr.setLimit(30); // The Choice table is huge, so I recommend setting a reasonable query limit. You should have an idea of the max # of results anyway.
            choiceGr.query();
            while (choiceGr.next()) {
                var choice = {};
                choice['value'] = choiceGr.getValue('value');
                choice['label'] = choiceGr.getValue('label');
                choiceArr.push(choice);
            }

            return JSON.stringify(choiceArr); // Return a stringified array to the client

        } else {
            gs.error('GetChoicesFromDT Script include did not run as the catItem mandatory variable is null: ' + catItem + ' or decision table sys_id is empty: ' + decisionTableId);
            return;
        }
    },

    type: 'GetChoicesFromDT'
});

 

 

Catalog Client Script

You need a client script to do 3 main things:

  1. Collect the necessary inputs from the form, such the sys_id of the catalog item and any variable values
  2. Send the inputs as parameters to the above Script Include
  3. Parse the response and add the choices to your field

 

onLoad

If you don’t need to wait for the user to fill in some variables to capture those as inputs, but this is already known when the form loads (such as the sys_id of the catalog item), then you can keep it simple and use an onLoad catalog client script to do all the above tasks. You will have to use GlideAjax to call your script include and process the response in a function.

In this case, I recommend that you add the variable along with the onLoad script into a variable set, so it can be very easily added to multiple catalog items.

 

onChange

An onChange catalog client script (only?) makes sense if you need to capture one or more values from other variables. You will need to decide which variable’s change will trigger the call of the script include, and you will have to account for the scenario of that (triggering) variable changing multiple times, even after you added the choices already.

 

Example

Here is an example that you can tailor to your needs for both an onLoad or an onChange script. It also includes some additional logic, such as setting the field read-only if there is only one choice, adding the “-- None --’’ option if there are more, etc. These are all examples - update it as per your own requirements.

 

 

 var targetChoiceField = ''; // Set this to the name of the Selectbox variable you want to populate
    g_form.clearOptions(targetChoiceField);

    var catItem = g_form.getUniqueValue();
    var dtChoiceAjax = new GlideAjax('global.GetChoicesFromDT'); // Set this to the name of the script include with the relevant scope
    dtChoiceAjax.addParam('sysparm_name', 'getChoices');
    dtChoiceAjax.addParam('sysparm_cat_item', catItem);
    /*
     * Add an other option parameter, e.g.:
     * dtChoiceAjax.addParam('sysparm_cat_variable', g_form.getValue('some_variable'));
     */
    dtChoiceAjax.getXMLAnswer(setChoices);

    function setChoices(answer) {
        if (answer) {
            var choiceArray = JSON.parse(answer);
            if (choiceArray.length == 0) {
                // Do something if the response is empty
                g_form.setReadOnly(targetChoiceField, false);
                g_form.setMandatory(targetChoiceField, false);
                g_form.setDisplay(targetChoiceField, false);
            } else {
                g_form.setDisplay(targetChoiceField, true);

                // Similarly, you might want to do something if there is only one choice, e.g. set that by default and make the field read-only. 
                var isSingleChoice = choiceArray.length == 1 ? true : false;
                if (isSingleChoice) {
                    g_form.addOption(targetChoiceField, choiceArray[0].value, choiceArray[0].label);
                    g_form.setValue(targetChoiceField, choiceArray[0].value);
                    g_form.setReadOnly(targetChoiceField, true);
                } else {
                    // And finally, if you have multiple options, decide how you want your field to behave
                    g_form.setReadOnly(targetChoiceField, false);
                    g_form.addOption(targetChoiceField, '', '-- None --'); // Adding None option - this is also optional
                    for (i = 0; i < choiceArray.length; i++) {
                        g_form.addOption(targetChoiceField, choiceArray[i].value, choiceArray[i].label, i + 1);
                    }
                    g_form.setMandatory(targetChoiceField, true); 
                }
            }
        } else {
			// What if there was no answer return from the script include at all?
            g_form.setReadOnly(targetChoiceField, false);
            g_form.setMandatory(targetChoiceField, false);
            g_form.setDisplay(targetChoiceField, false);
        }
    }

 

 

See it in action

Depending on the complexity of requirements, you may have this variable inside of a variable set along with an onLoad script, or standalone with a supporting onChange script. You can see how the different choices got rendered for the specific catalog items.

 

As reminder, here are the conditions defined in the decision table:

LaszloBalla_5-1708892385073.png

 

3M Privacy Filter - MacBook Pro:

LaszloBalla_6-1708892385070.png

 

3M Privacy Filter - MacBook Pro Retina - notice that the variable is read only since we only have once choice (as defined in the client script):

LaszloBalla_7-1708892385061.png

 

Acrobat:

LaszloBalla_8-1708892385119.png

 

QuickTime Pro:

LaszloBalla_9-1708892385084.png

 

Firewall Rule Change - for this one, we are considering another variable (namely the business_purpose variable inside the it_to_it variable set added to this item). As you can see, I have defined 2 specific options with one choice, and another scenario for the other two options, with two choices.

 

Special projects selected:

LaszloBalla_13-1708894200677.png

 

Testing selected:

LaszloBalla_14-1708894230572.png

 

KLTO or Other selected:

LaszloBalla_15-1708894255642.png

 

Summary

I hope this article serves as another illustration for the ultimate power that lies in leveraging Decision Tables. The biggest advantage is that you can delegate the maintenance of the table itself to any admin or business user, and let your underlying complex logic react to the changes in real time.

 

👇 Let me know in the comments if you think this example made sense, if you were able to use or re-use it, or share further suggestions of how you would extend or optimize it further (there is clearly room for that)! 

 

Comments
Sohail Khilji
Kilo Patron
Kilo Patron

Great effort @Laszlo Balla 

Laszlo Balla
ServiceNow Employee
ServiceNow Employee

And now the Table of Contents works too 😂

Rohant Joshi
Tera Contributor

Hi @Laszlo Balla,

This is great use case and you have covered it all, Thanks for the article.

I had question regarding UI Policy, can we use these dynamic choice values to configure the UI Policy in the condition filter?

Laszlo Balla
ServiceNow Employee
ServiceNow Employee

Thank you @Rohant Joshi.

This is an excellent question!

 

In the example scenario I have shared in this article, you will not be able to use the choices in a (Catalog) UI Policy condition, because the choices are not defined on the variable itself, they are only added by the client script (it's an illusion 😁).

 

You could, of course, define (all) the choices on the variable, and even try to refer that choice list in the decision table's result column. You could then change the client script to either remove all choices first (g_form.clearOptions()), and "add back" only the relevant ones, or reverse the logic to hide everything that is not relevant for that item using g_form.removeOption() (although for this one, you would probably want to change the script include too, to return what to hide as opposed to what to add).

Another option could be using a lookup selectbox, referencing the specific decision result elements table (var__m_sys_decision_multi_result_element_' + sys_id of decision table) and using a reference qualifier with a similar script include.

 

I am sure there are other options as well to make it work, and I am really glad to see people pushing the boundaries!

Vivek Verma
Mega Sage
Mega Sage

Brilliant Article.

I'm also a big fan of decision tables. We've been able to automate many of our business processes using decision management, which has reduced the delivery time significantly by requiring almost no code.

 

However, I have a doubt regarding the getDecisions API. The output array of this API is based on the order of the rows. Don't you think there should be an API that returns results based on relevance? For instance, the order of the returned array's elements. In my scenario, I have inputs that have multiple matches as output, and I would like to get the most relevant output as the first element in the output array. But as you know deciosnTable API code is restricted and we can't see any code behind those API working. so want to discuss if there is way to achieve this more effectively?

 

VivekVerma_0-1713093709023.png

so here if I pass input values like this, inpOne is blank, inpTwo is 2 and InpThree 3. then it gives the outPutCol value as 1 and 2. But can we get the most relevant decision first so 2 first then 1

Sridhar13
Tera Explorer

Great content, one question I have is how to sort the displayed choice values alphabetically

Mukesh Khanna P
Tera Expert

@Laszlo Balla, I'm having hundreds of choices that needs to be imported to the result choice field, but when I export the decision table excel file and update it with choice label and then try to import it, I'm getting the following error, the error is showing bcos the values are not there in the decision table, but how can we create hundreds of choices and then align that in decision table, kindly help me with this requirement.

MukeshKhannaP_0-1733244206799.png

 

Laszlo Balla
ServiceNow Employee
ServiceNow Employee

@Sridhar13 thanks for your feedback. I believe the easiest way to get your choices sorted by label would be to do this during the choiceGr GlideRecord in the Script Include, simply by using the orderBy method, e.g. adding something like this before kicking off the query:

choiceGr.orderBy('label');

Good point actually.

Laszlo Balla
ServiceNow Employee
ServiceNow Employee

@Mukesh Khanna P you need to import your choices separately into the sys_choice table (via import set + transform) an make sure the new choices have the "name" column set to the the following value:

var__m_sys_decision_multi_result_element_yourDecisionTableID

(obviously "yourDecisionTableID" must be the identifier of your decision table).

Then ideally you'll be able to see them in your decision table's choice result column.

Anna T_
Tera Expert

Hi @Laszlo Balla 

Thanks for the great article! I am doing something similar but setting choice fields on a demand record. The difference I have is that I have multiple result columns (i.e. each of the choice fields I want to populate).

I'm wondering what changes would be required to the on change client script and script include to be able to set choice options for multiple choice fields on the demand form.

Thanks!

Mike201
Tera Expert

This was fantastic. Thanks, but my dreams were crushed when I realized there was no option to present a catalog variable field that allows the user to select multiple options like a multi-select. All those options seem to expect a table to reference. 

Not sure if I should just create sys_choice fields or try to leverage the DecisionTableAPI createAnswerElementChoices method. 
😞

SushmaGM
Tera Explorer

This is really great article, it helped alot for me to configure .

@Laszlo Balla  am stuck in one place, result_elements always give NULL value for the resultcolumn though it shows the length but no value in the field. Any idea where am going wrong

 

Thanks,

Sushma G Muttalli

Laszlo Balla
ServiceNow Employee
ServiceNow Employee

@SushmaGM I guess we'd need to see your code (if you are still stuck) - care to share, or even better, do you have a dedicated community post about your issues (so a lot of other people might be able to help)? Let me know

Laszlo Balla
ServiceNow Employee
ServiceNow Employee

@Mike201 sorry about the later response - did you end up working around your issue? If so how? Otherwise if still relevant, I'd be curious to learn what is the actual requirement you are trying to fulfill.

Laszlo Balla
ServiceNow Employee
ServiceNow Employee

@Anna T_ did you solve your challenge with the multiple variable to multiple result column requirement? My initial though would be to extend the script include with an additional parameter that takes the technical name of the result column, and you map that to the relevant variable. Then you could have multiple calls from the client side to the same SI and simply providing the extra parameter with the result column name (or have a second decision table to keep the variable - result column mapping, hehe).

Anna T_
Tera Expert

@Laszlo Balla thanks for your response! We ended up going a different direction but this is good to know.

Mike201
Tera Expert

@Laszlo Balla I had a very long list of databases and the schemas available
Database | Schema
SQL1 | Users
SQL1 | Groups
SQL2 | Vendors
SQL2 | Contractors

I needed to create a catalog form with a field that lets you choose from a list of databases. When you choose the database the user needs to be able to multi-select from a long list of schemas. 

I was hoping you could populate catalog choices using the Decision Builder table. Instead, I ended up populating the Decision Table so that users could more easily add/remove. Then when the Decison Table updated a Business Rule refreshes a table of records used by the Catalog Form to populate the form fields. Hopefully, one day Decision Tables can be used to directly populate multi-selects, instead of custom tables or listing all of the options in the actual catalog item variables. 

Laszlo Balla
ServiceNow Employee
ServiceNow Employee

That’s an interesting one @Mike201, thanks for sharing!

Jacob Saaby Nie
Tera Contributor

Great article.

However I just discovered something else regarding choicelist results in Decision Tables.

I'm using a decision table to decide an incident state based on some input.

I'm basing the result on the state field of the incident table, which is an integer field. So it has a label and a value.

I assumed a decision table would always return the value, not the label.

Boy was I wrong. It does, in fact, return the label, not the value.

Bad decision, ServiceNow. Bad decision.

Version history
Last update:
‎12-12-2024 08:44 AM
Updated by:
Contributors