Integration
With
Amazon Alexa
v1.0
DISCLAIMER 4
OVERVIEW 5
USE CASES 9
REQUIREMENTS 9
RESOURCES 9
SERVICENOW SETUP 10
OAUTH 10
SCRIPTED REST API 11
AMAZON SETUP 13
CREATE NEW SKILL 13
INTERACTION MODEL 14
OAUTH AND REST ENDPOINT CONFIGURATION 16
SSL SETUP 17
APPENDIX A - SECURITY 54
BEST PRACTICES 54
PIN 54
APPENDIX B - TROUBLESHOOTING 55
Disclaimer
ServiceNow is changing the way people work. With a service-orientation toward the activities, tasks and processes
that make up day-to-day work life, we help the modern enterprise operate faster and be more scalable than ever
before.
The updates in this document represent our integration specs and assumptions, only as of the date of this
whitepaper. We undertake no obligation, and do not intend to update the forward‐looking changes, to review or
confirm expectations on installations completed.
Further information on these and other factors that could affect our technical outcomes are not intended to be
included.
The intent of this paper is to step through setup and creation of a skill in stages. The following
three scenarios are discussed:
1. Simple request/response based scenario.
2. Multi-step skill where state is remembered.
3. Refactoring to add:
a. Security – Authentication and authorization
b. Modularity – Script Includes
c. Domain separation - Scoped Applications
Skill Enablement
Once a skill is enabled and linked, Amazon manages the OAuth token automatically.
Skill Invocation
We will then combine that information to build a modular and secure scoped application that
allows for secured read and write operations to the ServiceNow instance.
This paper provides one possible solution to the use cases presented. It is intended to provide a
foundation to help understand the various moving parts when building an integration with
Alexa. It is expected that the implementer will build on this knowledge to craft a solution
custom to their needs. The solution should consider:
- Functional and non-functional requirements
- Scalability/performance
- Security
- Working within the framework provided by the ServiceNow platform.
When designing your skill, interaction refer to Amazon’s Voice Design Guide.
Resources
- How to Interact with Alexa - Amazon Alexa Voice Design Guide (Video)
- Choose the Invocation Name for a Custom Skill
- Alexa for BusinessAlexa for Business
o Publishing Private SkillsPublishing Private Skills
OAuth
Resources:
- ServiceNow - Product Documentation - OAuth 2.0
- ServiceNow - Product Documentation - OAuth authorization code grant flow
Steps
1. Log into your ServiceNow instance, for example, https://<yourinstance>.service-
now.com/
2. Go to the System OAuth > Application Registry.
3. Make sure you are in the global scope and not in an application scope.
4. Click button
5. Select the Create an OAuth API endpoint for external client’s option.
6. Enter the following values (or substitute your own values):
a. Name = Amazon Al exa
b. Client Secret = al exa
7. Take note of the fields just entered as they are custom to your implementation and
will be needed during the Amazon setup phase:
a. Client ID
b. Client Secret
8. You will need to come back and enter the redirect URL once you have completed the
Amazon setup.
7. Click Submit.
8. Reopen the record you just added.
9. Take note of the following field as it will be custom to your implementation:
a. Base API Path
10. Under the Resources tab, click the button
11. Enter the following values (or substitute your own values):
a. Name = Al exa - Snowy
b. HTTP Method = POST
c. Relative Path = /
d. Requires Authentication = Unchecked (See NOTE in step 1)
response.setStatus(200);
response.setContentType("application/json;charset=UTF-8");
var writer = response.getStreamWriter();
writer.writeString(global.JSON.stringify(alexaResponse));
// gs.info(global.JSON.stringify(alexaRequest.response));
return;
})(request, response);
NOTE: Replace host name with your instance using the path from the ServiceNow Setup -
> Scripted API setup above.
Do you allow users to create an Yes
account or link to an existing
account with you?
Authorization URL e.g. https://<yourinstance>.service-now.com/oauth_auth.do
2. You now need to take the redirect URLs from the Amazon Setup and update the
ServiceNow instance application registry setup in ServiceNow OAuth setup as described
above.
Certificate for DEFAULT My development endpoint is a sub-domain of a domain that has a wildcard certificate
Endpoint from a certificate authority
Basic setup is now complete. Once you have finished building the REST API endpoint you
will still need to deploy the skill to make it usable by others. This is covered in the Publishing
section.
Linking Account
You can wait to perform these steps until after you implement the security portion of the setup
below.
1. Navigate to your skill as described above.
2. Click Link Account to authenticate with the ServiceNow instance. You may need to try
this step a more than once.
You are now ready to test.
Echosim
If you do not have a device you can use https://echosim.io, a free, community-run project that
is an Echo in a webpage, for testing.
Overview
We are now going to build up our skill using Amazon’s schema.
Developer - Amazon - Alexa - JSON Interface Reference for Custom Skills
We will address the use case stated in the steps above.
if (alexaRequestType == 'LaunchRequest') {
alexaIntent = 'welcomeme';
}
alexaResponse.response.outputSpeech.type = "SSML";
if (alexaIntent == 'incident_search') {
var gr = new GlideRecord('incident');
gr.setLimit(5);
gr.query();
var out = "<speak>";
var i = 1;
var sysIds = [];
while (gr.next()) {
sysIds[i] = gr.getValue('sys_id');
out += '<say-as interpret-as="cardinal">' + i++ + '</say-as>. ';
out += gr.getValue("short_description");
out += ".";
}
out += "</speak>";
alexaResponse.sessionAttributes.incidentSysIds = sysIds;
alexaResponse.response.outputSpeech.ssml = out;
alexaResponse.response.shouldEndSession = false;
}
response.setStatus(200);
response.setContentType("application/json;charset=UTF-8");
var writer = response.getStreamWriter();
writer.writeString(global.JSON.stringify(alexaResponse));
// gs.info(global.JSON.stringify(alexaRequest.response));
return;
})(request, response);
3. Test your skill using the Service Simulator in the Amazon Developer Console.
a. Enter utterance: “list incidents.”
b. Look for a response like the following
{
"version": "1.0",
"response": {
"outputSpeech": {
"ssml": "<speak><say-as interpret-as=\"cardinal\">1</say-as>. Closed p1 - Critical incident Can't read
email.<say-as interpret-as=\"cardinal\">2</say-as>. On Hold p1 - Critical incident Unable to get to network file
shares.<say-as interpret-as=\"cardinal\">3</say-as>. In Progress p1 - Critical incident Wireless access is down in my
area.<say-as interpret-as=\"cardinal\">4</say-as>. Closed p1 - Critical incident Forgot email password.<say-as
interpret-as=\"cardinal\">5</say-as>. Closed p1 - Critical incident CPU load high for over 10 minutes.</speak>",
"type": "SSML"
},
"speechletResponse": {
"outputSpeech": {
"ssml": "<speak><say-as interpret-as=\"cardinal\">1</say-as>. Closed p1 - Critical incident Can't read
email.<say-as interpret-as=\"cardinal\">2</say-as>. On Hold p1 - Critical incident Unable to get to network file
shares.<say-as interpret-as=\"cardinal\">3</say-as>. In Progress p1 - Critical incident Wireless access is down in my
area.<say-as interpret-as=\"cardinal\">4</say-as>. Closed p1 - Critical incident Forgot email password.<say-as
interpret-as=\"cardinal\">5</say-as>. Closed p1 - Critical incident CPU load high for over 10 minutes.</speak>"
},
"shouldEndSession": false
}
// Given a week number return the dates for both weekend days
var getWeekendData = function(res) {
if (res.length === 3) {
var saturdayIndex = 5;
var sundayIndex = 6;
var weekNumber = res[1].substring(1);
return Dates = {
startDate : weekStart,
endDate : weekEnd,
};
}
};
// Given a week number return the dates for both the start date and the end
// date
var getWeekData = function(res) {
if (res.length === 2) {
var mondayIndex = 0;
var sundayIndex = 6;
var weekNumber = res[1].substring(1);
var weekStart = w2date(res[0], weekNumber, mondayIndex);
var weekEnd = w2date(res[0], weekNumber, sundayIndex);
return Dates = {
startDate : weekStart,
endDate : weekEnd,
};
}
};
/** ******************************************************* */
/* Mappings */
/** ******************************************************* */
var stateMappings = {};
stateMappings["new"] = 1;
stateMappings["in progress"] = 2;
stateMappings["on hold"] = 3;
stateMappings["resolved"] = 6;
stateMappings["closed"] = 7;
stateMappings["cancelled"] = 8;
/** ******************************************************* */
/* Customized Responses */
/** ******************************************************* */
var alexaRequest = request.body.data.request;
var alexaSession = request.body.data.session;
var alexaSessionAttributes = alexaSession.attributes;
var alexaRequestType = alexaRequest.type;
var alexaIntent = alexaRequest.intent.name;
var alexaIntentSlots = alexaRequest.intent.slots;
var alexaIntentContext = alexaSessionAttributes.intentContext;
if (alexaRequestType == 'LaunchRequest') {
alexaIntent = 'Help';
}
if (alexaIntent == 'AMAZON.StopIntent'
|| alexaIntent == 'AMAZON.CancelIntent') {
alexaResponse.sessionAttributes = {};
alexaResponse.response.outputSpeech.ssml = "<speak>Goodbye</speak>";
} else if (alexaIntent == 'AMAZON.HelpIntent' || alexaIntent == 'Help') {
var out = "<speak>";
out += "You can say, list incidents, to list incidents. You can list incidents specifying the
priority, state, and created date as filters. For example, list critical closed incidents created after last year.";
if (alexaIntentContext == 'incident_search') {
if (alexaSessionAttributes.sysIds.length) {
var indexes = alexaSessionAttributes.sysIds.length;
out += " You can get details about an incident. For example, get short
description for incident 1 based on your previous search.";
}
}
out += "</speak>";
// Keep session around so user can request details on data
alexaResponse.sessionAttributes = alexaSessionAttributes;
alexaResponse.response.outputSpeech.ssml = out;
} else if (alexaIntent == 'incident_detail') {
if (alexaSessionAttributes.table && alexaSessionAttributes.sysIds
&& alexaIntentSlots.index && alexaIntentSlots.index.value) {
var index = alexaIntentSlots.index.value;
var fieldName = fieldMappings[alexaIntentSlots.field.value];
var sysId = alexaSessionAttributes.sysIds.length > index ?
alexaSessionAttributes.sysIds[index]
: null;
if (alexaIntentSlots.priority
&& priorityMappings[alexaIntentSlots.priority.value]) {
gr.addQuery("priority", "=",
priorityMappings[alexaIntentSlots.priority.value]);
query += query.length == 0 ? "" : " and ";
query += " priority is " + alexaIntentSlots.priority.value;
}
gr.query();
var out = "<speak>";
var i = 1;
var sysIds = [];
if (gr.hasNext()) {
out += "Results matching " + query + ".";
} else {
out += "No data found for " + query;
}
while (gr.next()) {
sysIds[i] = gr.getValue('sys_id');
out += '<say-as interpret-as="cardinal">' + i++ + '</say-as>. ';
out += gr.getDisplayValue("incident_state") + ' p';
out += gr.getDisplayValue("priority") + ' incident ';
out += gr.getValue("short_description");
out += ".";
}
out += "</speak>";
response.setStatus(200);
response.setContentType("application/json;charset=UTF-8");
var writer = response.getStreamWriter();
writer.writeString(global.JSON.stringify(alexaResponse));
// gs.info(global.JSON.stringify(alexaRequest.response));
return;
})(request, response);
3. Test your skill using the Service Simulator in the Amazon Developer Console.
c. Enter utterance: “list critical closed incidents created after last year”
d. You should get back a response like the following
{
"version": "1.0",
"response": {
"outputSpeech": {
"ssml": "<speak><say-as interpret-as=\"cardinal\">1</say-as>. Closed p1 - Critical incident Reset my password.<say-as interpret-as=\"cardinal\">2</say-as>.
Closed p1 - Critical incident EMAIL is slow when an attachment is involved.<say-as interpret-as=\"cardinal\">3</say-as>. Closed p1 - Critical incident Missing my
home directory.<say-as interpret-as=\"cardinal\">4</say-as>. Closed p1 - Critical incident File Server is 80% full - Needs upgrade.<say-as interpret-
as=\"cardinal\">5</say-as>. Closed p1 - Critical incident Does not look like a backup occurred last night.</speak>",
"type": "SSML"
},
"speechletResponse": {
"outputSpeech": {
"ssml": "<speak><say-as interpret-as=\"cardinal\">1</say-as>. Closed p1 - Critical incident Reset my password.<say-as interpret-as=\"cardinal\">2</say-as>.
Closed p1 - Critical incident EMAIL is slow when an attachment is involved.<say-as interpret-as=\"cardinal\">3</say-as>. Closed p1 - Critical incident Missing my
home directory.<say-as interpret-as=\"cardinal\">4</say-as>. Closed p1 - Critical incident File Server is 80% full - Needs upgrade.<say-as interpret-
as=\"cardinal\">5</say-as>. Closed p1 - Critical incident Does not look like a backup occurred last night.</speak>"
},
"shouldEndSession": false
}
},
"sessionAttributes": {
"sysIds": [
null,
"46b66a40a9fe198101f243dfbc79033d",
"46cebb88a9fe198101aee93734f9768b",
"46e18c0fa9fe19810066a0083f76bd56",
"470af5afa9fe198101b324dd773ef379",
"470d51a0a9fe1981006c20c825e48933"
],
"query": "incident_state=7^priority=1.0^sys_created_on>=2016-01-01",
"createdAfter": "2016-01-01T00:00:00.000Z",
"table": "incident"
}
}
Requestor List incidents created by the requestor and get details about them.
Add Comments to the incident
Fulfiller List incidents assigned to the fulfiller
Add Comments to the incident
Manager List approvals waiting for manager.
Approve or Deny the request
Modularization/Scoped Applications
To make our code modular we need a way of mapping incoming requests to handlers. We can
use many of the concepts from HTTP handling. Conceptually we have:
- HTTP Request = Alexa Request
- HTTP Response = Alexa Response
- HTTP Session = Alexa Session
- URL = intent
/**
* Should process the request for the intent <intentname>
* @param ctx contextual access to helper functions, slot values and session attributes
Specifically, with the ServiceNow platform we will map packages to scoped applications. As
such we will need the following scoped applications.
Incident Integration
This scoped application will handle the Fulfiller and Requestor use cases
Approval Integration
This scoped application will handle the Manager use case
ServiceNow Platform Integration with Amazon Alexa - Whitepaper
© Copyright 2017 ServiceNow, Inc. All rights reserved. ServiceNow, the ServiceNow logo, and other ServiceNow marks are trademarks and /or registered trademarks of ServiceNow, Inc., in the United
States and/or other countries. Other company and product names may be trademarks of the respective companies with which they are associated.
ServiceNow Setup
Scoped Application: Alexa Skills Kit Integration
Scoped Application Creation
1. Go to “System Applications” -> “Applications”
2. Click Button
3. Select “Start From Scratch” and click the button
4. Enter values:
Field Value/Instructions
5. Click button
6. Click “OK”.
4. Click Button
5. Edit following values:
a. Main section
Field Value/Instructions
b. Add the two columns as shown as they are not created by default
Table Column name Type Reference Display
4. Click Button
5. Enter Following values and the script:
Field Value/Instructions
type : 'DateUtil',
/** ******************************************************* */
/* Common Functions */
/** ******************************************************* */
// Date source code sourced from:
// https://github.com/alexa/skill-sample-nodejs-calendar-reader/blob/master/src/index.js#L292
// Used to work out the dates given week numbers
w2date : function(year, wn, dayNb) {
var day = 86400000;
var j10 = new Date(year, 0, 10, 12, 0, 0), j4 = new Date(year, 0, 4,
12, 0, 0), mon1 = j4.getTime() - j10.getDay() * day;
return new Date(mon1 + ((wn - 1) * 7 + dayNb) * day);
},
// Given a week number return the dates for both weekend days
getWeekendData : function(res) {
if (res.length === 3) {
var saturdayIndex = 5;
var sundayIndex = 6;
var weekNumber = res[1].substring(1);
return {
startDate : weekStart,
endDate : weekEnd,
};
}
},
getDateFromSlot : function(rawDate) {
// try to parse data
var date = new Date(Date.parse(rawDate));
var result;
// create an empty object to use later
var eventDate = {};
6. Click Button
4. Click Button
5. Enter Following values:
Field Value/Instructions
6. Click button.
7. Click the button under the “Resources” tab.
8. Enter Following values and the script:
Field Value/Instructions
logDebugMessage(global.JSON.stringify(request.body.data));
/** ******************************************************* */
/* Initialization */
/** ******************************************************* */
// Request variable initialization
var alexaRequest = request.body.data.request;
var alexaRequestSession = request.body.data.session;
var alexaRequestSessionAttributes = alexaRequestSession.attributes;
var alexaRequestType = alexaRequest.type;
var alexaUser = request.body.data.session.user;
var alexaIntent = alexaRequest.intent.name;
/** ******************************************************* */
/* Processing */
/** ******************************************************* */
if (alexaRequestType == 'LaunchRequest') {
alexaIntent = 'Help';
}
if (alexaIntent == 'AMAZON.StopIntent'
|| alexaIntent == 'AMAZON.CancelIntent') {
alexaResponse.sessionAttributes = {};
alexaResponse.response.outputSpeech.ssml = "<speak>Cancelled</speak>";
} else if (alexaIntent == 'AMAZON.HelpIntent' || alexaIntent == 'Help') {
invokeHelp(alexaRequestSession, alexaRequest, alexaResponse);
} else {
// Locate handler and run
var cfg = {
filter : function(gr) {
gr.addQuery("intent", "=", alexaIntent);
}
};
var handlers = locateHandlers(cfg);
var handled = false;
// Copy input session variables to ouput response
alexaResponse.sessionAttributes = alexaRequestSessionAttributes;
for (i = 0; i < handlers.length; i++) {
var apiId = handlers[i].className;
var ctx = createContext(apiId, alexaRequestSession, alexaRequest,
alexaResponse);
var functionToCall = handlers[i][alexaIntent];
if (functionToCall) {
logDebugMessage('Calling Process for Handler:' + apiId);
functionToCall(ctx, alexaRequest, alexaResponse);
alexaResponse.sessionAttributes.handledBy = apiId;
handled = true;
break;
}
}
if (!handled) {
invokeHelp(alexaRequestSession, alexaRequest, alexaResponse);
}
}
response.setStatus(200);
response.setContentType("application/json;charset=UTF-8");
var writer = response.getStreamWriter();
var responseJSON = global.JSON.stringify(alexaResponse);
logDebugMessage(responseJSON);
writer.writeString(responseJSON);
return;
})(request, response);
9. Click Button
4. Go to the “Dependency” tab and click the button. Add the “Alexa Skills Kit
Integration” application.
// Process
var limit = 5;
var gr = new GlideRecordSecure('incident');
gr.setLimit(limit);
if (queryPreProcessor) {
queryPreProcessor.addFilters(gr);
}
gr.query();
var out = "<speak>";
var i = 0;
var sysIds = [];
if (gr.hasNext()) {
out += "Results matching " + query + ".";
} else {
out += "No data found for " + query;
}
out += "</speak>";
/** ******************************************************* */
/* Will locate a record stored in session state reading */
/* value from the attributes sysIds and table, the index */
/* slot is used to identify the sys id */
/** ******************************************************* */
function locateRecord(context, alexaRequest, alexaResponse) {
var sysIds = context.getSessionAttribute('sysIds');
var table = context.getSessionAttribute('table');
var slotSysIdIndex = context.getSlotValue('index', -1);
context.logDebugMessage("sysIds=" + sysIds);
context.logDebugMessage("table=" + table);
context.logDebugMessage("slotSysIdIndex=" + slotSysIdIndex);
// Process
var fieldName = fieldMappings[slotFieldName];
4. Go to the “Dependency” tab and click the button. Add the “Alexa Skills Kit
Integration” application.
if (queryPreProcessor) {
queryPreProcessor.addFilters(gr);
}
gr.query();
var out = "<speak>";
var i = 0;
var sysIds = [];
if (gr.hasNext()) {
out += "Results Found.";
} else {
out += "No data found.";
}
while (gr.next()) {
sysIds[i] = gr.getValue('sys_id');
out += '<say-as interpret-as="cardinal">' + i + '</say-as>. ';
out += gr.getDisplayValue("sysapproval.short_description");
out += ".";
i++;
}
out += "</speak>";
/** ******************************************************* */
/* Will locate a record stored in session state reading */
/* value from the attributes sysIds and table, the index */
/* slot is used to identify the sys id */
/** ******************************************************* */
function locateRecord(context, alexaRequest, alexaResponse) {
var sysIds = context.getSessionAttribute('sysIds');
var table = context.getSessionAttribute('table');
var slotSysIdIndex = context.getSlotValue('index', -1);
context.logDebugMessage("sysIds=" + sysIds);
context.logDebugMessage("table=" + table);
context.logDebugMessage("slotSysIdIndex=" + slotSysIdIndex);
// Process
var approvalState = approvalStateMappings[slotApprovalState];
if (approvalState) {
var gr = locateRecord(context, alexaRequest, alexaResponse);
gr.state = approvalState;
gr.update();
var out = "<speak>";
if (gr) {
out += approvalState + ' ' + gr.getDisplayValue("sysapproval.number");
}
out += "</speak>";
alexaResponse.response.outputSpeech.ssml = out;
alexaResponse.response.shouldEndSession = false;
}
return;
},
approval_search_manager_my_assigned : function(context, alexaRequest,
alexaResponse) {
var queryPreProcessor = {
addFilters : function(gr) {
gr.addQuery("approver", "=", gs.getUserID());
}
};
searchApprovals(context, alexaRequest, alexaResponse, queryPreProcessor);
},
};
approval_edit_state AlexaHandler_Approval
approval_search_manager_my_assigned AlexaHandler_Approval
incident_search_fulfiller_my_assigned AlexaHandler_Incident
incident_search AlexaHandler_Incident
incident_search_requestor_my_created AlexaHandler_Incident
incident_detail AlexaHandler_Incident
incident_edit_add_comment AlexaHandler_Incident
Name ask_snc_alexa_proxy
Role Create new role from template(s)
Role Name ask_snc_alexa_proxy_user
Policy Templates Simple Microservice permissions
function buildLinkAccountResponse() {
var alexaResponse = {
version : "1.0",
response : {
card : {
type : "LinkAccount"
},
outputSpeech : {
ssml : "<speak>Please link your account</speak>"
}
}
};
return alexaResponse;
}
// Check if access token is present and if not ask for account linking
if (!accessToken) {
callback(null, buildLinkAccountResponse());
return;
}
// Null out the access token so we don't get leakage during logging on ServiceNow instance
// end
event.session.user.accessToken = "";
event.context.System.user.accessToken = "";
res.on('end', function() {
var response = JSON.parse(body);
// Custom to ServiceNow REST enpoint
var error = response.error;
if (error) {
// Build link account card
response = buildLinkAccountResponse();
}
response.debug = {};
response.debug.attributes = {};
response.debug.attributes.sn_host = post_options.host;
response.debug.attributes.sn_api = post_options.path;
if (error) {
response.debug.attributes.sn_err = error;
}
console.info('OK: ' + JSON.stringify(response));
callback(null, response);
});
res.on('error', function(e) {
// On error send link acount card
var response = buildLinkAccountResponse();
response.debug = {};
response.debug.attributes = {};
response.debug.attributes.error = e;
console.log('ERROR: ' + JSON.stringify(response));
callback(null, response);
});
});
5. Click “Save”
List my created incidents List of incidents where the linked account holder is populated in the
caller_id is returned
List my assigned incidents List of incidents where the linked account holder is populated in the
assigned_to is returned
Add comment to 0 my comment is hello world For the cases above where an incident is returned the first incident is
updated with the comment hello world
List my approvals List of approvals where the linked account holder is populated in the
approver is returned
Deny change 0 For the cases above where an approval is returned the first approval is
updated with state deny
1. Once you are happy with your results go back through the code and
a. disable the logging
b. disable any unsecure REST endpoints from previous phases
2. Depending on your use cases you will likely be giving users access to sensitive data.
Please refer to Appendix A for best practices and guidelines to secure your skill further.
3. You can use the logging enabled in the various scripts and examine logs to see where
things may be going wrong. Refer to Appendix B for further help with debugging.
PIN
Alexa devices can be queried by anyone in their vicinity. In some cases, you may want to secure
retrieval or modification of sensitive information with a PIN or some other multi-factor
authentication method. The pin would be setup on the ServiceNow instance and requesting the
pin would be made part of the voice interaction.
ServiceNow believes information in this publication is accurate as of its publication date. This publication
could include technical inaccuracies or typographical errors. The information is subject to change without
notice. Changes are periodically added to the information herein; these changes will be incorporated in new
additions of the publication. ServiceNow may make improvements and/or changes in the product(s) and/or
the program(s) described in this publication at any time. Reproduction of this publication without prior
written permission is forbidden. The information in this publication is provided “as is”. ServiceNow makes no
representations or warranties of any kind, with respect to the information in this publication, and specifically
disclaims implied warranties of merchantability or fitness for a purpose.