You don't know ServiceNow APIs

GlideAggregate

    Recently, I gave an interview, where I was asked to write a script in scripts - background to print priority-wise count of all incident records. And I could not do it. Primarily because I did never use this API for anything else but getting the COUNT. So this is the time for doing what I did not do and probably you do not as well. So let us dig what this API is.

    If you did not get what exactly I was asked to do, follow this steps:

  • Navigate to Incident > All

1

  • Right-click the column Priority and select Group By Priority.

2

  • Your window will display something like this:

3

    Here, You will notice:

  • Groups are named for the values of the field selected for grouping. For example, in our case, since we grouped by Priority, each group is a Priority's display name.
  • The record count for each group appears next to the group name/Priority.
  • The total number of items in the list (all groups/Priorities combined) appears near the paging controls in the list.

    I had to write a script to print the output as shown in the last screenshot above. If you want to read more about "Grouped lists" you can find it here.

What is GlideAggregate

    GlideAggregate is an extension of GlideRecord. It provides the capability to do aggregation (COUNT, SUM, MIN, MAX, AVG). These operations can be done with regular GlideRecord use, but with GlideAggregate, they are optimized around these use cases and offer significant efficiencies.

Getting the number of records for GlideRecord query

    One of the frequently used use case is finding number of records for a a GlideRecord query. This can be achieved using getRowCount for a GlideRecord query, which is not a efficient solution if no operation is performed with the returned records. Alternatively, use of GlideAggreggate can improve the performance significantly. Let us see how to script both the ways:

getRowCount

  • Copy the below script to scripts - background, and click Run script button:
var grInc = new GlideRecord("incident")
grInc.query()
gs.info(grInc.getRowCount())

4

  • You should see the output similar to the following:

5

  • Now, Navigate to Incident > All

1

  • Right-click the value New for column State and select Show Matching.

7

  • Your window will display something like below, Observe that the condition State = New has been added to the filter and the total number of records with this filter:

8

  • Right-click the end of the filter breadcrumb and select Copy query from the context menu:

9

  • Now, let us modify the script as below to use encoded query copied above to filter the records:
var grInc = new GlideRecord("incident")
grInc.addEncodedQuery("state=1")
grInc.query()
gs.info(grInc.getRowCount())
  • Copy the above script to scripts - background, and click Run script button:

10

  • You should see the output similar to the following:

11

    This is the traditional way of getting number of records for a specific filter. You can dig documentation to know more about Copy query & Quick filters.

GlideAggregate

    There is alternate way of scripting the same using GlideAggregate that can improve the performance significantly and is more efficient:

  • Copy the below script to scripts - background, and click Run script button:
var grInc = new GlideAggregate("incident")
grInc.addEncodedQuery("state=1")
grInc.addAggregate("COUNT")
grInc.query()
if (grInc.next()) {
  gs.info(grInc.getAggregate("COUNT"))
}

12

  • You should see the output similar to the following:

13

    In the above script,

  • addAggregate() method is used to ask needed information.
  • getAggregate() method gets the value of an aggregate from the current record.

Group records

    Now, we can get back to our initial requirement:

  • Navigate to Incident > All 1

  • Right-click the column Priority and select Group By Priority. 2

  • Your window will display something like this: 3

  • Copy the below script to scripts - background, and click Run script button:

var grInc = new GlideAggregate("incident")
grInc.groupBy("priority")
grInc.addAggregate("COUNT")
grInc.query()
while (grInc.next()) {
  gs.info("Priority: {0} ({1})", [
    grInc.getDisplayValue("priority"),
    grInc.getAggregate("COUNT"),
  ])
}

14

  • You should see the output similar to the following:

15

    In the above script, groupBy() provides the name of a field to use in grouping the aggregates. Alternatively, we can use the second parameter of addAggregate() method to achieve the same result:

  • Copy the below script to scripts - background, and click Run script button:
var grInc = new GlideAggregate("incident")
grInc.addAggregate("COUNT", "priority")
grInc.query()
while (grInc.next()) {
  gs.info("Priority: {0} ({1})", [
    grInc.getDisplayValue("priority"),
    grInc.getAggregate("COUNT", "priority"),
  ])
}

16

  • You should see the output similar to the following:

17

getTotal()

    Let us modify our script to introduce another method, getTotal(), which returns the number of records by summing an aggregate:

  • Copy the below script to scripts - background, and click Run script button:
var grInc = new GlideAggregate("incident")
grInc.addAggregate("COUNT", "priority")
grInc.query()
while (grInc.next()) {
  gs.info("Priority: {0} ({1})", [
    grInc.getDisplayValue("priority"),
    grInc.getAggregate("COUNT", "priority"),
  ])
}
gs.info(grInc.getTotal("COUNT"))

18

  • You should see the output similar to the following:

19

groupBy() and COUNT

    At this point you should ask if there are two ways to group, what is the difference between them? Let us modify the script a bit to use both groupBy() and COUNT together:

  • Copy the below script to scripts - background, and click Run script button:
var grInc = new GlideAggregate("incident")
grInc.groupBy("priority")
grInc.groupBy("state")
grInc.addAggregate("COUNT", "priority")
grInc.query()
while (grInc.next()) {
  gs.info("Priority: {0} & State: {1} ({2})", [
    grInc.getDisplayValue("priority"),
    grInc.getDisplayValue("state"),
    grInc.getAggregate("COUNT", "priority"),
  ])
}
gs.info(grInc.getTotal("COUNT"))

20

  • You should see the output similar to the following:

21

    Here We did print number of records for each priority grouped by state. Let us verify the results:

  • Navigate to Incident > All

1

  • Right-click the column Priority and select Group By Priority.

2

  • Your window will display something like this:

3

  • Now, click the arrow icon next to the Priority 'Critical' to expand the group

22

  • Right-click the value In Progress for column State and select Show Matching.

25

  • You should see the output similar to the following:

23

24

Sorting the result

orderBy()

     Let us introduce another method called orderBy(), which provides the name of a field that should be used to order the aggregates.:

  • Copy the below script to scripts - background, and click Run script button:
var grInc = new GlideAggregate("incident")
grInc.groupBy("priority")
grInc.groupBy("state")
grInc.addAggregate("COUNT", "priority")
grInc.orderBy("priority")
grInc.orderBy("state")
grInc.query()
while (grInc.next()) {
  gs.info("Priority: {0} & State: {1} ({2})", [
    grInc.getDisplayValue("priority"),
    grInc.getDisplayValue("state"),
    grInc.getAggregate("COUNT", "priority"),
  ])
}
gs.info(grInc.getTotal("COUNT"))

26

  • You should see the output similar to the following:

27

     In the above example, we did sort the list by Priority and state.

orderByAggregate()

     Another variation of orderBy() is orderByAggregate(), which orders the aggregates based on the specified aggregate and field. Let us slightly modify our script:

  • Copy the below script to scripts - background, and click Run script button:
var grInc = new GlideAggregate("incident")
grInc.groupBy("priority")
grInc.groupBy("state")
grInc.addAggregate("COUNT", "priority")
grInc.orderByAggregate("count", "priority")
grInc.query()
while (grInc.next()) {
  gs.info("Priority: {0} & State: {1} ({2})", [
    grInc.getDisplayValue("priority"),
    grInc.getDisplayValue("state"),
    grInc.getAggregate("COUNT", "priority"),
  ])
}
gs.info(grInc.getTotal("COUNT"))

28

  • You should see the output similar to the following:

29

     You might have already noticed that the result is now sorted by the number of records.

Count isn't the only function

     So far we have seen some examples that utilizes COUNT for understanding GlideAggregate APIs, But Count is not the only thing that we can do with it. We can also use SUM, MAX, MIN, and AVG to get the total sum, maximum number, minimum number, and average, respectively. I would like to quote Andrew Barnes as is from his exteremely useful ServiceNow blog post "Understanding GlideAggregate" which can be found here:

Count isn’t the only function we can perform with GlideAggregate. Average, Sum, Max, Min are handy when dealing with numbers. Our favorite example table incident has several useful fields that pure counts isn’t wildly useful. Our service desk might want to know the average number of times incidents were modified sys_mod_count or reassigned reassignment_count. The service desk owner can use this information to set a baseline, and then run this every month to see if changes have been an improvement or not.

  • Copy the below script to scripts - background, and click Run script button:
var grInc = new GlideAggregate("incident")
grInc.groupBy("priority")
grInc.addAggregate("AVG", "reassignment_count")
grInc.addAggregate("AVG", "sys_mod_count")
grInc.query()
while (grInc.next()) {
  gs.info(
    "Incidents with priority {0} had {1} average modifications and {2} average reassignments",
    [
      grInc.priority.getDisplayValue(),
      grInc.getAggregate("AVG", "sys_mod_count"),
      grInc.getAggregate("AVG", "reassignment_count"),
    ]
  )
}

30

  • You should see the output similar to the following:

31

addHaving()

    addHaving() adds a "having" element to the aggregate. This method is extermely useful when you want to trigger something if record count crosses the defined threshold or to find duplicate records. You can find the awesome post on snprotips blog by Tim Woodruff here regarding same.

  • Copy the below script to scripts - background, and click Run script button:
var grInc = new GlideAggregate("incident")
grInc.groupBy("priority")
grInc.addAggregate("COUNT", "sys_mod_count")
grInc.addHaving("COUNT", "sys_mod_count", ">", "10")
grInc.query()
while (grInc.next()) {
  gs.info("Incidents with priority {0} had {1} total modifications", [
    grInc.priority.getDisplayValue(),
    grInc.getAggregate("COUNT", "sys_mod_count"),
  ])
}

32

  • You should see the output similar to the following:

33

    The above script outputs the total number of times particular priority incidents modified, if modification count is greater than 10. Feel free to change the value to something less like 1 or more like 30 and re-execute the script to observe the output.

addTrend()

    addTrend() method adds a trend for a field. Let us modify the script to display number of incidents by months.

  • Copy the below script to scripts - background, and click Run script button:
var grInc = new GlideAggregate("incident")
grInc.addTrend("opened_at", "month")
grInc.addAggregate("COUNT")
grInc.query()
while (grInc.next()) {
  gs.info("{0}: {1}", [grInc.getValue("timeref"), grInc.getAggregate("COUNT")])
}

34

  • You should see the output similar to the following:

35

getQuery()

    getQuery() method retrieves the query necessary to return the current aggregate.

  • Copy the below script to scripts - background, and click Run script button:
var grInc = new GlideAggregate("incident")
grInc.addTrend("opened_at", "month")
grInc.addAggregate("COUNT")
grInc.query()
while (grInc.next()) {
  gs.info("{0}: {1}, Query: {2}", [
    grInc.getValue("timeref"),
    grInc.getAggregate("COUNT"),
    grInc.getQuery(),
  ])
}

36

  • You should see the output similar to the following:

37


How can you learn more?

    The best way to learn anything is hands-on exercises. Here is some of the resources which will help provide examples and use cases to furthur enhance your understanding:

GlideExcelParser

    Recently, we did recieve a requirement to build a catalog item for Asset managers, to auto-import asset information in the platform based on attached excel sheet. Additionally, we had to reject the request if the data is missing or does not have referenced records available yet in the system.

    While exploring a bit, I found this amazing API called GlideExcelParser documented here. If you haven't used it yet like me, Let's explore together.

    If you are not a nerdy kind of guy, who can read the whole big article below; you might want to check short youtube video GlideExcelParser by Saikiran Guduri instead.

What is GlideExcelParser

    GlideExcelParser parses .xlsx formatted Excel files and access file data in a script. Although, GlideExcelParser methods can be used in both global and scoped scripts, you need to use the sn_impex namespace identifier to create a GlideExcelParser object.

    What it means is, to creates an instance of GlideExcelParser, we need to use the following syntax:

var parser = new sn_impex.GlideExcelParser();

Creating a demo data sheet

    Though, ServiceNow documentation is very clear and pretty self-explanatory (along with the awesome, easy to understand examples), what lacks for this API documentation is a demo data sheet to visualize it better. So before proceeding furthur, Let's create a demo data sheet for ourselves:

  • Navigate to All > Asset > Portfolios > All Assets. 1

  • Open the condition builder by clicking the show/hid filter icon; Build a condition as shown below and Click Run to apply the filter. 2

  • Click the personalize list icon in the upper left corner. 3

  • Remove the columns Assigned to and Configuration item. 4

  • To export a list report as an Excel spreadsheet, right-click any column heading and select Export > Excel. 5 6

  • Click Download. 7

  • Open the downloaded excel sheet. 8

  • Notice the column headings. 9

  • Let us apply some formatting to the excel sheet. However, it is not manadatory. 10

    I have marked Asset tag as Yellow to indicate it is a mandatory field. similarly, Model category, Display name (indicating Model) & Company are marked in Red to indicate they are both mandatory and reference fields. State, Substate & Cost are not mandatory and hence marked in Green.

Creating a fix script

    It is not necessary to create a fix script for our use case. But, it is always a good idea to test if our code is working as expected. And Fix script always seems to be a better choice than Background script cause of it's capabilities. Also, In order to test our APIs with the demo sheet that we did create, It needs to be in the platform as an attachment. Fix script can also solve that purpose in this case.

    If you haven't work with fix script earlier, please go and check out Community Live Stream - Topical Deep Dive - Fix scripts by Chuck Tomasi. If you did, let's go ahead and create a fix script and attach our demo sheet to it:

  • Navigate to All > System Definition > Fix Scripts & Click New. 11

  • Provide some meaningful Name & Description and Click Submit. 12

  • You will be redirected back to list view. Open the recently created fix script. 13

  • Click the attachments icon. 14

  • Click Choose Files or Browse, depending on your browser, and navigate to a file. Then, Click Attach. 15

  • Notice that our demo data sheet appears in the Current file attachments list; Close the dialog box. 16

  • Our demo data sheet also appears at the top of the form. 17

Testing GlideExcelParser APIs

    Now that we have our Fix script and demo data sheet attached to it, we are good to explore GlideExcelParser APIs. But, the very first thing that we need to test our APIs is the SysID of our demo data sheet attachment. Let's write the below code to our fix script to fetch the attachment SysID:

//Get attachment SysID

var attachmentSysID = "";

var tableName = "sys_script_fix";
var tableSysID = "19932bfc2f971110ccc9821df699b692";

var grAttachment = new GlideRecord("sys_attachment");
grAttachment.addEncodedQuery("table_name=" + tableName + "^table_sys_id=" + tableSysID);
grAttachment.query();
if (grAttachment.next()) {
    attachmentSysID = grAttachment.getUniqueValue();
}

gs.info("attachmentSysID: " + attachmentSysID);

    You should replace the value of tableSysID by SysID of your fix script. Right click on the form header and click Save to save your code.

code1

    To test if we are getting the SysID of our attachment, Click Run Fix Script button on the header and then select Proceed.

code2 code3

    As soon as the code finishes execution, we can see the output with the SysID of our attachment returned. Click Close to close the dialog window.

code4

GlideSysAttachment getContentStream()

    getContentStream() function of GlideSysAttachment accepts attachment sys_id as a parameter and returns a GlideScriptableInputStream object given the sys_id of an attachment that contains the content of an attachment. GlideSysAttachment APIs is a separate topic of discussion and we might talk about it in future articles, but for now, let us add following lines at the end of our script:

var attachment = new GlideSysAttachment();
var attachmentStream = attachment.getContentStream(attachmentSysID);
gs.info('Attachment content stream: ' + attachmentStream);

code5

    After executing the code again, you should get output similar to this:

code6

parse()

    In order to parse our demo data sheet we can use parse() function, that parses an XLSX-formatted Excel document provided as an input stream. let us add following lines at the end of our script:

var parser = new sn_impex.GlideExcelParser();
parser.parse(attachmentStream);
gs.info("typeof parser: "+typeof parser);
gs.info("parser: "+parser);

code7

    After executing the code again, you should get output similar to this:

code8

    And since now we know that parser is an object of type GlideExcelParser, we can play with this.

getColumnHeaders()

    getColumnHeaders() returns a list of column headers from the parsed document as an array of strings. In order to understand this better, let us comment the old log statements and add following lines at the end of our script:

var headers = parser.getColumnHeaders();
gs.info("headers: " + headers);
for (var i = 0; i < headers.length; i++) {
    gs.info("header " + (i + 1) + ": " + headers[i]);
}

code9

    After executing the code again, you should be able to see the list of all column headers from our attachments:

code10 code11

getSheetNames()

    getSheetNames() returns a list of all worksheet names, ordered as positioned from left to right in the workbook as an array of strings (including any worksheets marked as hidden). In order to understand this better, let us comment last four statements of our code and add following lines at the end of our script:

var list_sheet_name = parser.getSheetNames();
gs.info(" Sheet Names: " + list_sheet_name.join(", "));
for (var j = 0; j < list_sheet_name.length; j++) {
    gs.info("Sheet " + (j + 1) + ": " + list_sheet_name[j]);
}

code12

    After executing the code again, you should be able to see the list of all worksheets from our attachments. In our demo sheet there is only one worksheet and we can see it logged in our output:

code13 code14

next() & getRow()

    Now that we know how to get all column names, lets find out how to retrieve the information for each row. Just like GlideRecord, next() in GlideExcelParser is also used to loop through each of the row in an object. However, It is getRow() that actually returns the current row values and headers as an object. The row headers are stored as property names and the row values are stored as property values.

    Let's see this in action by commenting last five statements of our code and add following lines at the end of our script:

while (parser.next()) {
    var row = parser.getRow();
    gs.info("typeof row: " + typeof row);
    gs.info("row: " + row);
    gs.info("row: " + JSON.stringify(row, null, 4));
}

code15

    After executing the code again, you should get output similar to this:

code16

    Since we know that row is an javascript object, with column headers as keys and cell data as property values. We can easily retrieve the the sheet data by modifying the code inside while loop as below:

while (parser.next()) {
    var row = parser.getRow();
    //     gs.info("typeof row: " + typeof row);
    //     gs.info("row: " + row);
    //     gs.info("row: " + JSON.stringify(row, null, 4));
    var currentRowCellData = "";
    for (var header in headers) {
        var currentHeader = headers[header];
        currentRowCellData += row[currentHeader] + "\t";
    }
    gs.info("currentRowCellData: " + currentRowCellData);
}

code17

    After executing the code again, you should get output with all the cell data from our demo data sheet:

code18

Break Time

    Now that we have basic understanding of some of the APIs, It is good time to take a break and get back to our business requirement. Let us modify our code to check if any of the mandatory Asset Tag, Model or Model category is empty:

var mandatoryHeaders = ["Asset tag", "Display name", "Model category"];
while (parser.next()) {
    var row = parser.getRow();

    for (var header in mandatoryHeaders) {
        var currentHeader = mandatoryHeaders[header];

        if (JSUtil.nil(row[currentHeader])) {
            gs.info("No value provided for mandatory column: " + currentHeader);
        }

    }
}

code19

    If you have noticed already, I did remove all the comments from the code and modified the script within while loop to perform our validation for empty values of mandatory columns. And I have declared an additional array to store the mandatory column headers which has been used later in the loop. Here's how the whole script looks like:

var attachmentSysID = "";

var tableName = "sys_script_fix";
var tableSysID = "19932bfc2f971110ccc9821df699b692";

var grAttachment = new GlideRecord("sys_attachment");
grAttachment.addEncodedQuery("table_name=" + tableName + "^table_sys_id=" + tableSysID);
grAttachment.query();
if (grAttachment.next()) {
    attachmentSysID = grAttachment.getUniqueValue();
}

var attachment = new GlideSysAttachment();
var attachmentStream = attachment.getContentStream(attachmentSysID);

var parser = new sn_impex.GlideExcelParser();
parser.parse(attachmentStream);

var headers = parser.getColumnHeaders();

var mandatoryHeaders = ["Asset tag", "Display name", "Model category"];
while (parser.next()) {
    var row = parser.getRow();

    for (var header in mandatoryHeaders) {
        var currentHeader = mandatoryHeaders[header];

        if (JSUtil.nil(row[currentHeader])) {
            gs.info("No value provided for mandatory column: " + currentHeader);
        }

    }
}

    Now Let us modify our code to to add following lines to check if any of the mandatory Model or Model category is has a reference record existing in the system:

code20

    Here's how the whole script looks like:

var attachmentSysID = "";

var tableName = "sys_script_fix";
var tableSysID = "19932bfc2f971110ccc9821df699b692";

var grAttachment = new GlideRecord("sys_attachment");
grAttachment.addEncodedQuery("table_name=" + tableName + "^table_sys_id=" + tableSysID);
grAttachment.query();
if (grAttachment.next()) {
    attachmentSysID = grAttachment.getUniqueValue();
}

var attachment = new GlideSysAttachment();
var attachmentStream = attachment.getContentStream(attachmentSysID);

var parser = new sn_impex.GlideExcelParser();
parser.parse(attachmentStream);

var headers = parser.getColumnHeaders();

var mandatoryHeaders = ["Asset tag", "Display name", "Model category"];
while (parser.next()) {
    var row = parser.getRow();

    for (var header in mandatoryHeaders) {
        var currentHeader = mandatoryHeaders[header];

        if (JSUtil.nil(row[currentHeader])) {
            gs.info("No value provided for mandatory column: " + currentHeader);
        }

        if (checkReference(currentHeader, row[currentHeader]) === "NoReference") {
            gs.info("Referenced value " + row[currentHeader] + " does not exist for mandatory column: " + currentHeader);
        }

    }
}

function checkReference(cellHeader, cellData) {
    if (cellHeader == "Display name") {
        var grModel = new GlideRecord("cmdb_model");
        grModel.addQuery("display_name", cellData);
        grModel.query();
        if (!grModel.hasNext()) {
            return "NoReference";
        }
    }
    if (cellHeader == "Model category") {
        var grModelCategory = new GlideRecord("cmdb_model_category");
        grModelCategory.addQuery("name", cellData);
        grModelCategory.query();
        if (!grModelCategory.hasNext()) {
            return "NoReference";
        }

    }
    return "ReferencedRecordFound";
}

    Let us introduce a new variable rowNumber for better logging as below:

var rowNumber=1;
while (parser.next()) {
    var row = parser.getRow();

    for (var header in mandatoryHeaders) {
        var currentHeader = mandatoryHeaders[header];

        if (JSUtil.nil(row[currentHeader])) {
            gs.info("Row "+rowNumber+"1 - No value provided for mandatory column: " + currentHeader);
        }

        if (checkReference(currentHeader, row[currentHeader]) === "NoReference") {
            gs.info("Row "+rowNumber+"1 - Referenced value " + row[currentHeader] + " does not exist for mandatory column: " + currentHeader);
        }

    }
 rowNumber++;
}

code21

    Let us introduce another variable insufficientData to check if manadatory column is empty or reference does not exist:

var insufficientData="FALSE";
while (parser.next()) {
    var row = parser.getRow();

    for (var header in mandatoryHeaders) {
        var currentHeader = mandatoryHeaders[header];

        if (JSUtil.nil(row[currentHeader])) {
            gs.info("Row "+rowNumber+" - No value provided for mandatory column: " + currentHeader);
   insufficientData="TRUE";
        }

        if (checkReference(currentHeader, row[currentHeader]) === "NoReference") {
            gs.info("Row "+rowNumber+" - Referenced value " + row[currentHeader] + " does not exist for mandatory column: " + currentHeader);
   insufficientData="TRUE";
        }

    }
 rowNumber++;
}

code22

    Before testing the code, we need to make two more changes for our use case. We will analyze the whole sheet for not sufficient data and will log the information only once. Let us modify the code one more time as below:

code23

    Now our final code should look like this:

var attachmentSysID = "";

var tableName = "sys_script_fix";
var tableSysID = "19932bfc2f971110ccc9821df699b692";

var grAttachment = new GlideRecord("sys_attachment");
grAttachment.addEncodedQuery("table_name=" + tableName + "^table_sys_id=" + tableSysID);
grAttachment.query();
if (grAttachment.next()) {
    attachmentSysID = grAttachment.getUniqueValue();
}

var attachment = new GlideSysAttachment();
var attachmentStream = attachment.getContentStream(attachmentSysID);

var parser = new sn_impex.GlideExcelParser();
parser.parse(attachmentStream);

var headers = parser.getColumnHeaders();

var mandatoryHeaders = ["Asset tag", "Display name", "Model category"];
var rowNumber = 1;
var insufficientData = "FALSE";
var incorrectDataLog = "Incorrect Data: \n";
while (parser.next()) {
    var row = parser.getRow();

    for (var header in mandatoryHeaders) {
        var currentHeader = mandatoryHeaders[header];

        if (JSUtil.nil(row[currentHeader])) {
            incorrectDataLog += "Row " + rowNumber + " - No value provided for mandatory column: " + currentHeader + "\n";
            insufficientData = "TRUE";
        }

        if (checkReference(currentHeader, row[currentHeader]) === "NoReference") {
            incorrectDataLog += "Row " + rowNumber + " - Referenced value " + row[currentHeader] + " does not exist for mandatory column: " + currentHeader + "\n";
            insufficientData = "TRUE";
        }

    }
    rowNumber++;
}

if (insufficientData !== "TRUE") {
    incorrectDataLog = "CorrectData";
}
gs.info(incorrectDataLog);


function checkReference(cellHeader, cellData) {
    if (cellHeader == "Display name") {
        var grModel = new GlideRecord("cmdb_model");
        grModel.addQuery("display_name", cellData);
        grModel.query();
        if (!grModel.hasNext()) {
            return "NoReference";
        }
    }
    if (cellHeader == "Model category") {
        var grModelCategory = new GlideRecord("cmdb_model_category");
        grModelCategory.addQuery("name", cellData);
        grModelCategory.query();
        if (!grModelCategory.hasNext()) {
            return "NoReference";
        }

    }
    return "ReferencedRecordFound";
}

Creating and testing Invalid Demo Data

    Now that we have our code ready to be tested, we need a demo data sheet with some missing or incorrect information. Let's modify the original sheet by deleting some of the mandatory information:

test1

    Next, remove the old attachment and replace it with new attachment having incorrect data:

test2

    Executing the code again, should didplay the output like this:

test3

    But if you are really paying an attention, you will notice that it is Row 8 and not Row 7 which is actually empty. Same applies for other rows. The reason for this is because the first row of excel is column headers and not actually a data. Let's fix it with just increasing the initial value of rowNumber by 1.

test4

    Another thing that you should notice is, The API parses all the Rows in the sheet even if they are totally empty (E.g. Row 8). Unfortunately, there is no OOTB workaround for this issue available yet. You can read more about the issue here. Though, we can skip those rows with a little trick. Let us write an additional line of code in our script:

test5

    At the end of this section, your final code should be somewhat similar to this:

var attachmentSysID = "";

var tableName = "sys_script_fix";
var tableSysID = "19932bfc2f971110ccc9821df699b692";

var grAttachment = new GlideRecord("sys_attachment");
grAttachment.addEncodedQuery("table_name=" + tableName + "^table_sys_id=" + tableSysID);
grAttachment.query();
if (grAttachment.next()) {
    attachmentSysID = grAttachment.getUniqueValue();
}

var attachment = new GlideSysAttachment();
var attachmentStream = attachment.getContentStream(attachmentSysID);

var parser = new sn_impex.GlideExcelParser();
parser.parse(attachmentStream);

var headers = parser.getColumnHeaders();

var mandatoryHeaders = ["Asset tag", "Display name", "Model category"];
var rowNumber = 2;
var insufficientData = "FALSE";
var incorrectDataLog = "Incorrect Data: \n";
while (parser.next()) {
    var row = parser.getRow();

    if (JSON.stringify(row, removeNullValues) === "{}") {
        continue;
    }

    for (var header in mandatoryHeaders) {
        var currentHeader = mandatoryHeaders[header];

        if (JSUtil.nil(row[currentHeader])) {
            incorrectDataLog += "Row " + rowNumber + " - No value provided for mandatory column: " + currentHeader + "\n";
            insufficientData = "TRUE";
        }

        if (checkReference(currentHeader, row[currentHeader]) === "NoReference") {
            incorrectDataLog += "Row " + rowNumber + " - Referenced value " + row[currentHeader] + " does not exist for mandatory column: " + currentHeader + "\n";
            insufficientData = "TRUE";
        }

    }
    rowNumber++;
}

function removeNullValues(key, value) {
    if (value != null) {
        return value;
    }
}

if (insufficientData !== "TRUE") {
    incorrectDataLog = "CorrectData";
}
gs.info(incorrectDataLog);


function checkReference(cellHeader, cellData) {
    if (cellHeader == "Display name") {
        var grModel = new GlideRecord("cmdb_model");
        grModel.addQuery("display_name", cellData);
        grModel.query();
        if (!grModel.hasNext()) {
            return "NoReference";
        }
    }
    if (cellHeader == "Model category") {
        var grModelCategory = new GlideRecord("cmdb_model_category");
        grModelCategory.addQuery("name", cellData);
        grModelCategory.query();
        if (!grModelCategory.hasNext()) {
            return "NoReference";
        }

    }
    return "ReferencedRecordFound";
}

    If you dont have any idea about how json works or why we have written the code, I highly recommend you to checkout my post on community - What You Need To Know About JSON And Objects.

Building a Data Source and Transform Map

    So we have our script to validate the Excel sheet. But, we did not do anything for loading the excel data to the platform. Let's build a data source and transform map, which we can use later in our script to automate the process.

  • Navigate to All > System Import Sets > Administration > Data Sources and Click New. import1

  • Provide some meaningful Name & Import set table label. Select other details as below before clicking Submit:

    • Type: File
    • Format: Excel
    • Sheet number: 1
    • Header row: 1
    • File retrieval method: Attachment

    import2

  • You will be navigated back to list view. Open the recently created Data source record. import3

  • Add our demo sheet as an attachment to the current record. import4

  • Click Test Load 20 Records related link at the bottom of the form. import5

    The Test Load 20 Records UI action is generally used to validate the records that you want to import without the initial intention of running a transformation map to the target table. This action sets the internal state of the import to Load, therefore the transformation map is skipped. There are two good articles regarding this on NowSupport & Community that you must read.

  • Once data is loaded, click the Create transform map link. import6

  • Provide some meaningful Name and Select the Target table as Asset, then right click on the form header and select Save. import7

  • Click the Auto Map Matching Fields Related Link for ServiceNow to attempt to match the staging table columns to the target table columns based on column name. import8

  • Set the Coalesce value to true to use an Asset tag field to check for collisions. import9

    If a match is found using the coalesce fields, the target record is updated with the information imported from the staging table. If no match is found, a new record is inserted in the database. If no fields are coalesce fields, records are always inserted on import. You can read more about coalescing on developer portal.

  • Note down the Import set table name from data source and SysID of recently created transform map for later reference (we need it!). script2 script1

    If you did not use data source or transform map earlier Importing Data is a good place to start.

Building Catalog item and Workflow

    Now that we have all pre-requisites (a validation script, a data source and a transform map), we can go ahead and create a Catalog item to realize our business requirement.

  • Navigate to Service Catalog > Catalog Definition > Maintain Items & Click New. cat1

  • Provide some meaningful Name, Short Description & Description. Select other details as below and Save the form:

    • Catalogs: Technical Catalog
    • Category: Infrastructure
    • Fulfillment automation level: Fully automated

    cat2

  • Under Portal Settings section, Select Request method as Submit and mark Hide 'Add to Wish List' & Mandatory Attachment as checked. cat3

  • Under Process Engine, click the magnifying glass besides Workflow field. cat4

  • Click New cat5

  • Provide some meaningful Name and Description and click Submit. cat6

  • The new workflow is created with the Begin and End activities connected by a single transition. cat7

  • Switch to the Core tab in the Palette. Click, hold, and drag the Run Script activity onto the canvas. Hover the activity over a transition. When the transition turns blue, drop the activity. cat8

  • Add some meaningful Name and Stage. Then Copy the final fix script created in first step and paste here in the Script field. cat9

  • Change the values of variable tableName & tableSysID as below:

var tableName = current.sys_class_name;
var tableSysID = current.sys_id;

cat10

  • Since we already know the list of mandatory columns to be validated; Let's remove the following line of code as we are not going to need it.
var headers = parser.getColumnHeaders();

cat11

  • Let's comment the log statements and add the following line of code at the end of the script, to store the additional comments in the scratchpad if data is not sufficient or incorrect. Then Click Submit.
workflow.scratchpad.insufficient_data = insufficientData;
workflow.scratchpad.incorrect_lata_log = incorrectDataLog;

cat12

  • Double click the If activity. cat13

  • Give some meaningful Name and Stage. cat14

  • Select the Advanced check box and add the following script to the Script before clicking Submit.

answer = ifScript();

function ifScript() {
    if (workflow.scratchpad.insufficient_data === "TRUE") {
        return 'yes';
    }
    return 'no';
}

cat15

  • Next, Double click on Set Values activity and set the values as below, then click Submit:

    • Name: Closed Incomplete
    • Stage: Completed
    • Set these values:
      • State: Closed inomplete
      • Additional comments: ${workflow.scratchpad.incorrect_lata_log}

    cat16

  • Similarly, Double click on Set Values activity again and set the values as below, then click Submit:

    • Name: Closed complete
    • Stage: Completed
    • Set these values:
      • State: Closed complete

    cat17

  • Finally, Double click the new Run script activity. Add the following script to the Script field:

//Create a Data Source and Copy an attachment from RITM to Data Source
var donorTable = current.sys_class_name;
var donorID = current.sys_id;

var recipientTable = "sys_data_source";

var recipientTableGR = new GlideRecord(recipientTable);
recipientTableGR.name = gs.getUserName() + " UserImport at: " + new GlideDateTime();
recipientTableGR.import_set_table_name = 'u_auto_import_assets_data'; 
recipientTableGR.file_retrieval_method = "Attachment";
recipientTableGR.type = "File";
recipientTableGR.format = "Excel";
recipientTableGR.header_row = 1;
recipientTableGR.sheet_number = 1;

var recipientID = recipientTableGR.insert(); //Need this since we want to load and transform directly

GlideSysAttachment.copy(donorTable, donorID, recipientTable, recipientID);

//Load the data from attachment to Staging table
var dataSourceGR = new GlideRecord("sys_data_source");
dataSourceGR.get(recipientID);
var loader = new GlideImportSetLoader();
var importSetRec = loader.getImportSetGr(dataSourceGR);
var ranload = loader.loadImportSetTable(importSetRec, dataSourceGR);
importSetRec.state = "loaded";
importSetRec.update();

//Transform the data into system database
var transformMapSysIDs = '1f25c5492f1f1110ccc9821df699b642';

var transformWorker = new GlideImportSetTransformerWorker(importSetRec.sys_id, transformMapSysIDs);
transformWorker.setBackground(true);
transformWorker.start();

    For the sake of this article, I'm not going to explain the above script details. May be it could be a content for another article. But to be short we are loading the data from our sheet to import set table that we created and then transforming it to assets table. To understand better what is happening here, I encourage you to check teh youtube video Upload data with an excel file and Record producer in ServiceNow by Göran Lundqvist.

  • Don't forget to replace the Import set table name and Transform Map SysID noted from second step before clicking Submit: cat18 cat19

  • Now configure transitions for all activities as shown in the screenshot below: cat20

  • Let's navigate back to our catalog item window and select the newly created workflow and Update the catalog item. cat21

  • In order to see the Technical Catalog on our Service Portal, we need to do some additional configurations. Navigate to All > Service Portal > Portals & Click the Service Portal record. cat23

  • In the Catalogs related list, click Edit. cat24

  • Select Technical Catalog to add to the portal and click Save cat25

    Well we are done with our development and it is a time for unit testing:

  • Navigate to Service Portal and click Request Something. cat22

  • Now, we should be able to see our newly created catalog item under Infrastructure category of Technical Catalog. Click to open it. cat26

  • Add our incorrect demo data sheet as an attachment and click Submit. cat27

  • Click on the catalog item name on the Ticket summary form to see the additional comments with error logs: cat28 cat29

  • Perform another test with correct sheet to make sure that data is loaded correctly to asset table: cat30 cat31

Time's up!

    So we have successfully fulfilled our business requirement. For the sake of this article, we have done very basic configurations. But in real life scenerio, you might need to have error handling mechanism, additional approvals, user criterias etc. But we won't go to that. Now, it is time to go back to our GlideExcelParserAPIs.

    Let us modify our incorrect demo data sheet before proceeding furthur. Add another sheet with some demo data. E.g.:

after1

    Now navigate back to our fix script created in step one and replace the attachment with new updated sheet:

after2

    Lets remove the entire code from line number 19, as we do not need it for furthur testing:

after3

    Our new script now should look like this:

var attachmentSysID = "";

var tableName = "sys_script_fix";
var tableSysID = "19932bfc2f971110ccc9821df699b692";

var grAttachment = new GlideRecord("sys_attachment");
grAttachment.addEncodedQuery("table_name=" + tableName + "^table_sys_id=" + tableSysID);
grAttachment.query();
if (grAttachment.next()) {
    attachmentSysID = grAttachment.getUniqueValue();
}

var attachment = new GlideSysAttachment();
var attachmentStream = attachment.getContentStream(attachmentSysID);

var parser = new sn_impex.GlideExcelParser();
parser.parse(attachmentStream);

after4

setSheetName() & setSheetNumber()

    setSheetName() sets the name of the sheet to be retrieved. Whereas, setSheetNumber() sets the number of the Excel sheet to be retrieved. However, It worths remembering that if both setSheetNumber() and setSheetName() are set, setSheetName() is used.

    Add the following lines of code at the end of our sheet to retrive all sheet names:

// Get the worksheet names to be parsed in the Excel document
var list_sheet_name = parser.getSheetNames();

gs.info(" Sheet Names " + list_sheet_name.join(", "));

after5

    After executing the code, we should be able to see both of our the sheet names in the output:

after6

    Now to test setSheetName first, Let's modify our code as below:

var attachmentSysID = "";

var tableName = "sys_script_fix";
var tableSysID = "19932bfc2f971110ccc9821df699b692";

var grAttachment = new GlideRecord("sys_attachment");
grAttachment.addEncodedQuery("table_name=" + tableName + "^table_sys_id=" + tableSysID);
grAttachment.query();
if (grAttachment.next()) {
    attachmentSysID = grAttachment.getUniqueValue();
}

var attachment = new GlideSysAttachment();
var attachmentStream = attachment.getContentStream(attachmentSysID);

var parser = new sn_impex.GlideExcelParser();
parser.setSource(attachmentStream);
parser.setSheetName("Page 2");
parser.parse();

// Get the worksheet names to be parsed in the Excel document
// var list_sheet_name = parser.getSheetNames();

// gs.info(" Sheet Names " + list_sheet_name.join(", "));

var headers = parser.getColumnHeaders();
while (parser.next()) {
    var row = parser.getRow();
    var currentRowCellData = "";
    for (var header in headers) {
        var currentHeader = headers[header];
        currentRowCellData += row[currentHeader] + "\t";
    }
    gs.info("currentRowCellData: " + currentRowCellData);
}

after7

    After executing the code, we should be able to see the data from sheet "Page 2" :

after8

    If you are paying an attention so far, we have an additional method setSource that we did not yet discussed and slightly different version of parse (without parameter); but they play very important role in this case. We will talk about them next, but first let test our next API i.e. setSheetNumber.

    Let's modify our code to use setSheetNumber instead of setSheetName. Please note that 0 denotes sheet 1, 1 denotes sheet 2 & so on...:

parser.setSheetNumber(1);

after9

    After executing the code, we should be able to see the same output as above:

after10

    It worth mentioning that, it is not necessary to use setSource and we can as well do it the old way as shown below:

var attachmentSysID = "";

var tableName = "sys_script_fix";
var tableSysID = "19932bfc2f971110ccc9821df699b692";

var grAttachment = new GlideRecord("sys_attachment");
grAttachment.addEncodedQuery("table_name=" + tableName + "^table_sys_id=" + tableSysID);
grAttachment.query();
if (grAttachment.next()) {
    attachmentSysID = grAttachment.getUniqueValue();
}

var attachment = new GlideSysAttachment();
var attachmentStream = attachment.getContentStream(attachmentSysID);

var parser = new sn_impex.GlideExcelParser();
parser.setSheetNumber(1); 
parser.parse(attachmentStream);

// Get the worksheet names to be parsed in the Excel document
// var list_sheet_name = parser.getSheetNames();

// gs.info(" Sheet Names " + list_sheet_name.join(", "));

var headers = parser.getColumnHeaders();
while (parser.next()) {
    var row = parser.getRow();
    var currentRowCellData = "";
    for (var header in headers) {
        var currentHeader = headers[header];
        currentRowCellData += row[currentHeader] + "\t";
    }
    gs.info("currentRowCellData: " + currentRowCellData);
}

after11 after12

setSource()

    setSource() defines an input source for parsing multiple times or parsing each worksheet in an Excel file.

    E.g. In our demo sheet, we have two sheets "Page 1" & "Page 2". We can use setSource to parse an Excel file attachment multiple times to retrieve column headers and print values of each row for each worksheet.

    Let's copy a part of code and paste it again at the end of the script and just change the sheet number. We will get some warnings as we are not following the coding best practices, but let's ignore them for a moment:

var attachmentSysID = "";

var tableName = "sys_script_fix";
var tableSysID = "19932bfc2f971110ccc9821df699b692";

var grAttachment = new GlideRecord("sys_attachment");
grAttachment.addEncodedQuery("table_name=" + tableName + "^table_sys_id=" + tableSysID);
grAttachment.query();
if (grAttachment.next()) {
    attachmentSysID = grAttachment.getUniqueValue();
}

var attachment = new GlideSysAttachment();
var attachmentStream = attachment.getContentStream(attachmentSysID);

var parser = new sn_impex.GlideExcelParser();
parser.setSource(attachmentStream);
parser.setSheetNumber(1);
parser.parse();

var headers = parser.getColumnHeaders();
while (parser.next()) {
    var row = parser.getRow();
    var currentRowCellData = "";
    for (var header in headers) {
        var currentHeader = headers[header];
        currentRowCellData += row[currentHeader] + "\t";
    }
    gs.info("currentRowCellData: " + currentRowCellData);
}

parser.setSheetNumber(0);
parser.parse();

var headers = parser.getColumnHeaders();
while (parser.next()) {
    var row = parser.getRow();
    var currentRowCellData = "";
    for (var header in headers) {
        var currentHeader = headers[header];
        currentRowCellData += row[currentHeader] + "\t";
    }
    gs.info("currentRowCellData: " + currentRowCellData);
}

after13

    Observe that we did parse each of the sheet using same parser and were able to retrieve the data:

after14

    Lets modify our code to be better and parse all sheet's data automatically:

var attachmentSysID = "";

var tableName = "sys_script_fix";
var tableSysID = "19932bfc2f971110ccc9821df699b692";

var grAttachment = new GlideRecord("sys_attachment");
grAttachment.addEncodedQuery("table_name=" + tableName + "^table_sys_id=" + tableSysID);
grAttachment.query();
if (grAttachment.next()) {
    attachmentSysID = grAttachment.getUniqueValue();
}

var attachment = new GlideSysAttachment();
var attachmentStream = attachment.getContentStream(attachmentSysID);

var parser = new sn_impex.GlideExcelParser();
parser.setSource(attachmentStream);

// Get the worksheet names to be parsed in the Excel document
var list_sheet_name = parser.getSheetNames();
gs.info(" Sheet Names " + list_sheet_name.join(", "));

// Iterate over each worksheet in the Excel workbook
for (var i = 0; i < list_sheet_name.length; i++) {

    gs.info("Sheet name:    " + list_sheet_name[i]);

    // Set the worksheet name to be parsed
    parser.setSheetName(list_sheet_name[i]);

    parser.parse();

    var headers = parser.getColumnHeaders();
    while (parser.next()) {
        var row = parser.getRow();
        var currentRowCellData = "";
        for (var header in headers) {
            var currentHeader = headers[header];
            currentRowCellData += row[currentHeader] + "\t";
        }
        gs.info("currentRowCellData: " + currentRowCellData);
    }

}

after15

    Observe that we did parse each of the sheet using same parser and were able to retrieve the data:

after16

    The best example for this API can be found at Developer portal, I encourage you to have a look at that.

    The one of the important point to note here (and you might have already understood): we don't pass parameter to parse when used along with setSource.

setNullToEmpty()

    setNullToEmpty() accepts one parameter which when true, return an empty value instead of null when an Excel cell is not present.

    In our excel sheet there are certain cells that are empty and our script currently prints them as null:

after17

    Lets add the following line of code to our script:

parser.setNullToEmpty(true);

after18

    Now, executing the script again should display empty value instead of null:

after19

close()

    As name suggests, close() close the connection to the input stream and release the document.

    All we need to do is just adding the following line at the end of the code:

parser.close();