ServiceNow Scripted REST APIs & Retrieving Catalog Item Variables

In Fuji and prior releases, ServiceNow has supported the REST API, but they've only supported simple table and record APIs using GET, PUT, POST, PATCH, and so on. In Geneva however, ServiceNow introduced Scripted REST Apis.

Scripted REST APIs allow a developer to create a custom REST API Resource, receive request arguments via a REST call, and return data (as with GET requests), create a record (as with POST requests), update records (as with PUT/PATCH requests), or delete records (using a - you guessed it - DELETE request) using custom code that can utilize REST headers, query parameters, and/or request body elements

Scripted REST APIs have only become available fairly recently (in the Geneva release of ServiceNow), and are something that a lot of developers have been avoiding, as they're new and arcane, and the documentation around them is still fairly new, and not very well built-out.
In this article, we're going to learn how to create our own custom scripted REST API, which we'll use for our demo project: Retrieving the service catalog variables associated with a given RITM, for reporting in an external system. 

Pro Tip: If you came here just wanting to deploy this functionality, skip to the bottom of this article, where you'll find information on obtaining a pre-built update set that you can deploy into your instance to enable retrieving catalog item variables via REST

Before we get started, I want to quickly make clear what is, and is not, available in ServiceNow by default. 
If you've only upgraded to Geneva, there are no scripted REST APIs available by default (except maybe a "user role inheritance" internal API), so you'll pretty much have to write each one yourself. 

However, if you're on Helsinki, there is a "Service Catalog REST API", which allows you to do things like add a catalog item to a cart, update a cart, delete a catalog item from a cart, submit a cart, submit a record producer, and loads more. This is not mentioned in any of the documentation that I've found, and I only learned about it by exploring in my developer instance while writing this article! So if you are just trying to submit catalog items via REST, now you know! There's a way (and a very robust way at that) to do it in Helsinki. 

One thing that's missing, even from the Helsinki scripted REST APIs however, is what we're going to build today -- A simple way to retrieve service catalog variables associated with a given RITM. 


What is REST?

To understand scripted REST APIs, we have to first understand what REST is, and how it's used in ServiceNow. 

I'm not going to go into great depth here about REST, but suffice it to say that REST is a stateless, cacheable "language" that clients and servers can use to talk to one another, and pass data back and forth. You can send a request to a REST resource via an API. REST requests have a few key components:

  • Resource URI: This is the "Unique Resource Identifier". It usually begins with "HTTP://", and it looks like a web address. In fact, it is a web address! If you were to take a simple rest GET request for example, and put it into your browser's URL bar, you would get the results of that request in your browser window! 
  • Request Header: This is a set of key-value pairs attached to the request, and is not unlike the header on a webpage. It contains some metadata about the request, such as an authorization token, what "language" (Content-Type) the request is in, and what Content-Type the request will accept a response in. 
  • Request Body: The Body of the request is where the bulk of the data can be found. If you're sending a "POST" request for example, which might generate a record on a server, the body of the request might contain the contents of the record to be created. 
  • Query Parameters: Query parameters are values added onto the URI of a webpage or, in this case, a REST call. A parameter would look like this: 
    http://site.com/api/v1/resource?sys_id=D-1


Similar to how variables work in javascript, request headers, parameters, and the request body can be used to pass data between the client and server. There are a few simple tools that you can use to build REST requests. One of my favorite, is called PostMan for Chrome, and it is free. 

Let's Get Started!

The first thing that we have to do to get started, is navigate to System Web Services -> Scripted Web Services -> Scripted REST APIs, from the Application Navigator.

Right away, you'll notice some significant differences, depending on whether you're in Geneva, or Helsinki or later. In this article, I'm going to be focusing on the Geneva functionality, because it requires more manual work. It should be almost the same, but a smidge easier, in Helsinki. The code will be the same. 

While Helsinki has some Scripted REST APIs already in place, in Geneva, you should be presented with an essentially empty list. To begin the creation of your REST API, click on New, and fill out a name and API ID. I'm going to use ritm_vars_api (Same as my API name) but you can call yours whatever you like. The API ID will identify your API specifically, and will be part of the API URI. Your API URI will be something like this, when we're through: 
http://your_instance.service-now.com/api/your_company_identifier/API_ID

Save your new Scripted REST Service record, and you'll have 3 new tabs, and 3 related lists. The related lists represent REST resources, headers, and query parameters (described above).

As we build a scripted API to send over the variables and variable values associated with a given request item, we'll need to create a way for the requestor to pass in the RITM ticket numbers they'd like to retrieve the variables for.
We could use query parameters, but let's make use of a request header instead. That just feels more formal to me; you can build your API however you like, so long as you document it! 

To create a request header, simply select the related list, and click New. The header name is like a variable value, and the user will populate it with the RITM numbers of the tickets they'd like to get the variables for. I created one that looks like this: 

As you can see from the example value, I'm allowing the user to pass in multiple ticket numbers, or just one. The list should be comma-separated. This will make it easier on the requestor if they're generating the API call from a script, because they can just pass an array into the header value and javascript will coerce it into a comma-delimited string in order to conform to the application/json content-type.

And that's pretty much all the data we need from the user for this Scripted REST Service - so let's define our resource. 

Back on the Scripted REST Service page, open up the Resources related list, and click New. On this page, we need to define the Http method (get, post, put, etc.), the resource name, and the script that'll do the work for us. 

For the name I entered Get RITM Variables. For the Http Method, I selected GET. In the Documentation section, I entered "This resource returns the variables related to the RITM you've specified.". 

Now all we have to do is define a script which will get the request header, and define the response so that it contains the results that the requestor is looking for. This is the scripted part of Scripted REST APIs. When you create a new resource, you'll be given a script stub that looks something like this: 

(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {

    // implement resource here

})(request, response);

From this stub, we can tell that we've got access to two parameters that we can make use of, inside the process function: request (a RESTAPIRequest object), and response (a RESTAPIResponse object). If you read our article, What's New in Geneva/UI16, you know that the Geneva script editor is far more powerful now, than it used to be. We can make use of the intellisense style code-completion, to see what methods of the RESTAPIRequest and RESTAPIResponse objects we have access to, by typing "request." or "response." like so:

From this, we can learn a few things about how this all works -- for example, we can use request.getHeader('ritm') to get the value of the RITM header that the user sent along with their request! 

Here's my code for processing these requests: 

(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response)
{
    var responseObj = {}; //The object that should ultimately be returned.
    var ritmNumbers;
    
  //Set ritmNumber from header value, convert to an array, and trim each element.
    ritmNumbers = trimAndSplit(request.getHeader('ritm'), ','); //Calling custom script include.
    //ritmNumbers = request.getHeader('ritm').split(','); //<--Commented out, but you can enable it if you don't have the trimAndSplit script include.
  
    if (!ritmNumbers) {
        //If ritm header was not set or was malformed, return an error.
        response.setStatus(417);
        return 'Unable to process request. Request header \'ritm\' not defined. ' +
            'Please define a request header called \'ritm\' with the value set to the ticket number for the RITM you\'d like to retrieve the variables for. Value provided for RITM: ' + ritmNumbers;
    }
    //Declare the var that'll hold the RITM number while we iterate down below.
    var ritm;
    //declare the var that'll hold the variables object (derived from gr.variables);
    var ritmVariables;
    //Get a GlideRecord to start things up
    var gr = new GlideRecord('sc_req_item');
    //declare variables object. This will be used to temporarily store variable values later, and is necessary because of how JS handles objects.
    var variablesObject;
    //For each RITM in the ritmNumbers array...
    for (var i = 0; i < ritmNumbers.length; i++) {
        //Get the RITM number
        ritm = ritmNumbers[i];
        //Get the GlideRecord for the RITM, or throw an error if we can't.
        if (!gr.get('number', ritm)) { //If we can't find the RITM...
            response.setStatus(417); //set the status to indicate a bad argument
            response.setHeader(ritm, 'Unable to locate GlideRecord for ritm header argument: ' + ritm); //add a header to indicate which RITM was bad.
            gs.logError('Scripted REST API ritm_vars_api unable to process request. RITM not found: ' + ritm); //Log an error so the admin knows what happens if the requestor asks.
            continue; //Continue the loop with the next RITM.
        }
        //Get the object containing the catalog variables.
        ritmVariables = gr.variables;
        //Declare a fresh object each loop.
        //This will be made to contain all of the variables and variable values for the RITM we're iterating over,
        //Then it will be pushed into the responseObj object, and reset for the next RITM on the next iteration.
        variablesObject = {};
        //Set the 'number' property on the variables object to the current ritm number. This will also be the
        variablesObject["number"] = ritm;
        //Iterate over ritmVariables, looping through each one as v.
        //This is necessary because the "gr.variables" is not a standard JS object, and cannot be mapped.
        for (var v in ritmVariables) {
            if (ritmVariables.hasOwnProperty(v) && ritmVariables[v]) { //Make sure the property exists and isn't null or unknown.
                //NOTE: If you want to return all variables on the catalog item associated with the RITM, remove the second condition in this IF block above.
                //With the second condition, this will only show variables that have been populated.
                var variableName = v.toString(); //Make sure we're all proper strings here.
                //pushing the variable into variablesObject, which will be copied into responseObject along with a version of variablesObject for each of the RITMs.
                variablesObject[variableName] = ritmVariables[variableName].toString();
            }
        }
        //Call addObjToObj function to add variablesObject to responseObj with the key set to the current RITM number, so it can be accessed as 'responseObj["RITM0123456"]'.
        //NOTE: If we didn't use addObjToObj, we'd run into this problem where objects are linked rather than copied, when added to other objects in javascript.
        responseObj = addObjToObj(responseObj, variablesObject, ritm);
    }
    response.setHeader('Notes', 'Returning only those variables which are populated. To get all variables, update the ritm_vars_api GET resource, and change the condition on 66 to \'true\'');
    return responseObj; //Returning the variables and their values. Returning an object here will set the response "body" to the JSON equivalent of this object. 


})(request, response);

function addObjToObj(parent, child, name)
{
    parent[name] = child;
    return parent;
}

You can read through the code comments to get a really good sense of what's going on, but basically this script just grabs the value associated with the request header from the request that triggered the REST service to run, then queries the database for the values of the variables associated with the RITMs listed in that header. Once it has them, it returns all the variables and values that have been populated as an object which, in JSON, is returned as the body of the response back to the requestor. Simple! 

Also, On line 6 in my RESt resource, I make use of a custom script include that I wrote called trimAndSplit, so here's the code for that as well: 

/**
 * Converts a string to an array, much like .split(), except that it trims each element of leading and trailing whitespace.
 * @param str {string} The string that should be split and trimmed
 * @param [tok=,] {string} The character to split on (usually ','). Default value is a comma character.
 * @returns {Array} An array of elements that have each been trimmed of whitespace.
 */
function trimAndSplit(str, tok)
{
    if (!str || typeof str !== 'string') {
        gs.logError('trimAndSplit function received an invalid argument in the first argument. The argument received was: ' + str + 
                   '. The only acceptable first argument for trimAndSplit, is a string.');
        return;
    }
    if (!tok || tok.length != 1) {
        tok = ',';
    }
    if (str.indexOf(tok) < 0) {
        return str; //Return the original string, because no instances of the token were found.
    }
    //declare return array
    var ret = [];
    //convert the input string to an array, splitting on the given token (usually ',')
    var ara = str.split(tok);
    var ele;
    for (var i = 0; i < ara.length; i++) {
        ele = ara[i];
        //Trim each element in the split string array, then push it to the return array.
        ret.push(ele.trim());
    }
    //return the trimmed and split array.
    return ret;
}

And that's about all there is to it! You can access this API using a tool like the one I mentioned near the beginning of this article, by using a request like this: 

Pro Tip: In the URI next to GET in the above screenshot, the "my_company" bit can be replaced with the API namespace value from your Scripted REST Service page. 

If you were to run a request like the one above against an instance that had this scripted REST API enabled, you would get a response that looked something like this:

{
  "result": {
    "RITM0087924": {
      "phone": "123456",
      "short_description": "Mobile App Deployment",
      "email": "fgj@fgj.com",
      "request_type": "Mobile App Deployment",
      "description": "test",
      "requested_by": "0b7bb7840a0a3cd10145e7471b1dbe57",
      "number": "RITM0087924",
      "vp": "0b5672ce0a0a3cd0011356150042b37a"
    },
    "RITM0087923": {
      "phone": "5122122155",
      "short_description": "Mobile App Deployment",
      "email": "gul@ghm.com",
      "request_type": "Mobile App Deployment",
      "description": "test by ghm",
      "project_id": "146bd15bhf01ddc0e00334afae3ee4e2",
      "requested_by": "fc46dfhh6fedda805d0be981be3ee43e",
      "number": "RITM0087923",
      "vp": "0b5h72ce0a0a3cd1011356150042b37a",
      "requested_for": "0b7bb7940hha3cd10145e7471b1dbe57"
    }
  }
}

In a few days, you'll be able to download this tool, pre-built, so you can deploy it into your instance. Just keep an eye on the "Tools" drop-down at the top of this page. 

That's all! We put a lot of work into these articles, so if you liked it, we humbly welcome you to subscribe to see more. If your company needs help implementing, enhancing, or building new ServiceNow solutions, we also humbly welcome you to get in touch with us to discuss how we might be able to help you out!