Integrating ServiceNow with SAP (S4 Hana Cloud)
If you don't think your client has SAP, think again. The German behemoth boasts that over 400,000 companies use its software. Bill McDermott, the current CEO of ServiceNow, served as SAP's very first American CEO up through 2019, when he left for ServiceNow. With such a storied background, it should be little surprise that business clients use SAP - quite possibly even including your own company! SAP is everywhere, so let's dive into why it's so important...
The core driver behind SAP's dominance as a platform is their ability to enhance back-end business processes. While SAP excels at these processes, the front-end can be a bit clunky and disconnected. That's why it's an ideal fit for integrating with ServiceNow using the Employee Service Center as the process origin-point and approval flow.
Unfortunately, despite excellent documentation, SAP's jungle of products and versions became quite the confusing mess to figure out. Having someone who’s familiar with the other tool and is willing to lend a hand is a huge asset when it comes to integration. One of the SAP Admins for our client was instrumental in helping us get this project done.
The integration
We began our work for the client with the goal of managing their SAP Purchase Order requests from within the ServiceNow interface.
This effort was focused around requesting a Purchase Order via the Employee Service Center in ServiceNow. In order to achieve this, we needed to introduce a Catalog Item which could display remote SAP items and eventually transmit new Purchase Orders back to SAP. Integrating SAP via Catalog Items enabled creation of powerful workflows, such as the handling of approvals entirely within ServiceNow, before anything ever gets touched on SAP
Authentication
The SAP API is session-based, in other words: before anything else, we must present our credentials and request a new session cookie. This posed an issue, because ServiceNow's user-friendly REST Flows do not expose cookies! The solution here was for us to leverage the lower-level RESTMessageV2 API using JavaScript, which exposed for us the very helpful getCookies method.
var cookies = j2js(response.getCookies());
//Extracting context number from cookies
var contextCookiePrefix = "sap-usercontext=";
var maybeContextNumber = Stream.fromArray(cookies)
.find(function (cookie) {
return cookie.startsWith(contextCookiePrefix);
})
.map(function (contextCookie) {
var cookieContent = contextCookie.slice(contextCookiePrefix.length);
var cookieData = cookieContent.split(";")[0];
var contextNumber = cookieData.trim().replace(/^sap-client=/, "");
return contextNumber;
});
if (maybeContextNumber.isEmpty()) {
// This should never happen
gs.error(
new NiceError(
"Server response OK but could not find context number given the following cookies: {0}"
),
JSUtil.describeObject(cookies)
);
}
//Looking for session cookie
var maybeSessionCookie = maybeContextNumber.flatMap(function (contextNumber) {
return Stream.fromArray(cookies).find(function (cookie) {
return cookie.startsWith("SAP_SESSIONID_NED_" + contextNumber + "=");
});
});
Content Length
Another showstopper was the APl's mandatory requirement to pre-compute Content-Length headers on outgoing HTTP messages. This posed another problem because ServiceNow does variable substitution at the very last moment, after our Data Source's last chance to set Content-Length!
We eventually bypassed this issue by manually applying body variable substitution – in JavaScript – long before ServiceNow would normally do it. This required some fairly creative code to pull off:
var templateParameters = {
netPriceAmount: ritmGr.variables.product_cost,
// ... and so forth. This is a mapping
// Keys corresond to Outbound Message variables
// Values correspond to Request Item variables
glAccount: ritmGr.variables.gl_account.u_node_name.toString(), // "545208",
};
// Format template parameters as data acceptable by GlideStringUtil.substituteVariables
var jProperties = new Packages.java.util.Properties();
for (var key in templateParameters) {
var value = String(templateParameters[key]);
jProperties.put(key, value.replace('"', '\\"'));
request.setStringParameter(key, value);
}
// Do variable substitution for the body early so we can accurately calculate content-length
var templateContent = request.getRequestBody();
var content = j2js(GlideStringUtil.substituteVariables(templateContent, jProperties));
// SAP blows up if the JSON payload contains whitespace, so guard against that here
var minifiedContent = JSON.stringify(JSON.parse(content));
request.setRequestBody(minifiedContent);
var contentLength = minifiedContent.length;
request.setRequestHeader('Content-Length', contentLength);
Data Sources
Since the Catalog Item needed SAP data, we created multiple scripted Data Sources to pull over the data from SAP, leveraging as much of the Out of box REST Message functions as possible.
var message = new sn_ws.RESTMessageV2('SAP - PO', 'Get Cost Center');
var resultStream = sappo_streamData(message);
resultStream.forEach(function(result) {
import_set_table.insert({
"currency": result["CostCntrCurr"],
"rollup": result["RollUp"],
"number": result["NodeName"],
"name": result["NodeDesc"],
"area": result["ControllingArea"],
"id": result["ID"],
});
});
Although these elements were brought over for this Catalog Item, some of them were mapped to out of box tables (Vendors -> core_company, Cost Centers -> cmn_cost_center...), and help supplement the existing user data that we were getting.
Conclusion
Integrating with SAP is possible, but not easy. However, if you (or your client) want ServiceNow to be THE customer-facing portal, then an integration to ERP tools - such as SAP - will be a necessity.
Building an integration like this will enable you to harness ServiceNow’s Service Catalog, Portal, Workflow Engine, and notifications to maintain a consistent branding and messaging across all platforms, even though the final fulfillment of this request will still occur in SAP.