Custom Integrations
Scope
This page helps you to start using the Integration API functionality in order to add custom code to b+s Connects, by providing examples and ideas.
For more information please refer to the Integration API documentation.
Limitations
In order to send requests/receive events from b+s Connects, the Custom Integration code must be hosted within ServiceNow.
Requirements
In order to start implementing custom code the following requirements must be met:
Create a UI Page
The
cnx.connectsIntegrationLibrary.min.js
script must be included. It is located here: https://connects.bucher-suter.com/snow/v4/stable/cnx.connectsIntegrationLibrary.min.jsProvide a callback function as argument to the
initIntegration
method on theConnectsIntegrationLibrary
class (property on window object). The provided callback will be called once the Integration API is ready to be utilized.
Interface overview
The methods to invoke and subscribe to the Integration API are exposed by the class ConnectsIntegrationLibrary
.
Requests
All requests on the ConnectsIntegrationLibrary
return a promise. The data returned by the promise has the following structure:
Field | Description |
---|---|
success | Boolean indicating whether the request was successful or not |
response | Object containing the data as outlined in the overview |
Events
Multiple event handlers can be registered for the same event. The event data is described in the overview.
Setup the example integration
Step 1: Create a UI Page and insert the following code (adjust [reason code]
with respective values).
<?xml version="1.0" encoding="utf-8" ?>
<j:jelly trim="false" xmlns:j="jelly:core" xmlns:g="glide" xmlns:j2="null" xmlns:g2="null">
<head>
<meta charset="utf-8"></meta>
<title>Example of a Custom Integration</title>
<script src="https://connects.bucher-suter.com/snow/v4/stable/cnx.connectsIntegrationLibrary.min.js"></script>
<script>
const cil = ConnectsIntegrationLibrary;
console.warn(`message: waiting for initIntegration`);
cil.initIntegration(() => {
console.warn('message: initIntegration ready');
cil.onError((error) => console.warn('message: onError data: ' + JSON.stringify(error)));
cil.onAgentStateChange((data) => console.warn('message: onAgentStateChange, data: ' + JSON.stringify(data)));
cil.onWorkitemCreate((data) => console.warn('message: onWorkitemCreate, data: ' + JSON.stringify(data)));
cil.onWorkitemConnect((data) => console.warn('message: onWorkitemConnect, data: ' + JSON.stringify(data)));
cil.onWorkitemPause((data) => console.warn('message: onWorkitemPause, data: ' + JSON.stringify(data)));
cil.onWorkitemResume((data) => console.warn('message: onWorkitemResume, data: ' + JSON.stringify(data)));
cil.onWorkitemEnd((data) => console.warn('message: onWorkitemEnd, data: ' + JSON.stringify(data)));
cil.onQueueDataUpdate((data) => console.warn('message: onQueueDataUpdate, data: ' + JSON.stringify(data)));
cil.onWorkitemWrapup((data) => console.warn('message: onWorkitemWrapup, data: ' + JSON.stringify(data)));
cil.onCallVariableChanged((data) => console.warn('message: onCallVariableChanged, data: ' + JSON.stringify(data)));
cil.onWrapupDataChanged((data) => console.warn('message: onWrapupDataChanged, data: ' + JSON.stringify(data)));
});
async function getChannel() {
const result = await cil.getChannel('voice');
console.warn(result);
console.warn('message: getChannel result '+ JSON.stringify(result));
return result;
}
async function getReasonCodeList(reasonType) {
const channel = await cil.getChannel('voice');
cil.getReasonCodeList(
channel.response.channel.id,
reasonType
).then((data) => {
console.warn('message: getReasonCodeList ' + JSON.stringify(data));
});
}
async function setWrapupReason() {
const channel = await cil.getChannel('voice');
if (!channel.response.channel.workitems.length) {
console.error('The function setWrapupReason needs a workitem id as parameter. So a call is needed to use this button.');
return;
}
cil.setWrapupReason(
channel.response.channel.id,
channel.response.channel.workitems[0].id,
'Generic'
).then((data) => {
console.warn('message: setWrapupReason ' + JSON.stringify(data));
});
}
async function isUpdateWorkitemDataEnabled() {
const channel = await cil.getChannel('voice');
if (!channel.response.channel.workitems.length) {
console.error('The function isUpdateWorkitemDataEnabled needs a workitem id as parameter. So a call is needed to use this button.');
return;
}
cil.isUpdateWorkitemDataEnabled(
channel.response.channel.id,
channel.response.channel.workitems[0].id
).then((data) => {
console.warn('message: isUpdateWorkitemDataEnabled ' + JSON.stringify(data));
});
}
async function updateWorkitemData(requestId) {
const channel = await cil.getChannel('voice');
if (!channel.response.channel.workitems.length) {
console.error('The function updateWorkitemData needs a workitem id as parameter. So a call is needed to use this button.');
return;
}
cil.updateWorkitemData(
channel.response.channel.id,
channel.response.channel.workitems[0].id,
{ callVariable1: 'Updated value' },
requestId
).then((data) => {
console.warn('message: update workitem ' + JSON.stringify(data));
});
}
async function setAgentState(newState, reasonCode, requestId) {
const channel = await cil.getChannel('voice');
cil.setAgentState(
channel.response.channel.id,
newState,
reasonCode,
requestId
).then((data) => {
console.warn('message: setAgentState ' + JSON.stringify(data));
});
}
async function dialOrConsult(requestId) {
cil.dialOrConsult(
'voice',
document.getElementById('phoneNum').value,
requestId
).then((data) => {
console.warn('message: dial ' + JSON.stringify(data));
});
}
async function directTransfer(requestId) {
cil.directTransfer(
'voice',
document.getElementById('phoneNum').value,
undefined,
requestId
).then((data) => {
console.warn('message: directTransfer ' + JSON.stringify(data));
});
}
async function singleStepConference(requestId) {
cil.singleStepConference(
'voice',
document.getElementById('phoneNum').value,
undefined,
requestId
).then((data) => {
console.warn('message: singleStepConference ' + JSON.stringify(data));
});
}
async function isDtmfEnabled() {
cil.isSendDtmfEnabled().then((data) => {
console.warn('message: isSendDtmfEnabled ' + JSON.stringify(data));
});
}
async function sendDtmf(requestId) {
cil.sendDtmf(
document.getElementById('phoneNum').value,
requestId
).then((data) => {
console.warn('message: sendDtmf ' + JSON.stringify(data));
});
}
</script>
<style>
body {
margin: 0;
}
.wrapper {
padding: 2px;
display: flex;
flex-wrap: wrap;
}
button {
width: calc(100% - 4px);
margin: 2px;
background-color: #fff;
color: #333;
border: 1px solid #777;
cursor: pointer;
}
input {
width: calc(100% - 8px);
margin: 10px 2px 2px 2px;
}
</style>
</head>
<body>
<div class="wrapper">
<button onclick="setAgentState('ready', undefined, 'some rid3')">Ready</button>
<button onclick="setAgentState('notready', '[reason code]', 'some rid1')">Not Ready (Lunch)</button>
<button onclick="setAgentState('logout', '[reason code]', 'some rid2')">Logout (1) </button>
<button onclick="getChannel()">GetChannel</button>
<button onclick="getReasonCodeList('[reason type]')">Get Reason Code List</button>
<button onclick="setWrapupReason()">Set WrapUp Reason</button>
<button onclick="isUpdateWorkitemDataEnabled('some rid10')">Update Workitem Data enabled?</button>
<button onclick="updateWorkitemData('some rid9')">Update Workitem Data</button>
<input id="phoneNum" placeholder="Enter phone number or DTMF here"></input>
<button onclick="dialOrConsult('some rid4')">Call/Consult</button>
<button onclick="directTransfer('some rid5')">SST</button>
<button onclick="singleStepConference('some rid6')">SSC</button>
<button onclick="isDtmfEnabled('some rid7')">DTMF enabled?</button>
<button onclick="sendDtmf('some rid8')">Send DTMF</button>
</div>
</body>
</j:jelly>
Step 2: Save the customization.
Step 3: Add the corresponding HTTP Request Header for the saved UI page. For detailed instructions please read this chapter.
Step 4: Configure the Custom Integration in the Service Layout. For detailed instructions please read this chapter.
HTTP Response Header
ServiceNow role required: security_admin
In order to load the custom integration, an additional HTTP response header is needed. It can be created within the sys_response_header
table.
The following values need to be set specifically:
Field | Value |
---|---|
Active | True |
Applies to | Specific Type |
Type | UI Page [sys_ui_page] |
Record | Select the UI page that you want to integrate within the gadget. |
Name | Content-Security-Policy |
Value | frame-ancestors 'self' https://connects.bucher-suter.com |
Add by | Overwrite * |
Order | Higher value than existing entry for teams.microsoft.com * |
* These values are edited in the table view, not in the editing form.
If multiple UI pages are created for custom integrations, one response header needs to be created per UI page.
In the ServiceNow Utah release, the built-in HTTP response header for teams.microsoft.com
comes preconfigured in the sys_response_header
table. To avoid conflicts with b+s Connects custom integrations, the configuration must comply with the following:
- The custom header for b+s Connects has a higher
Order
than the built-in header. - The custom header for b+s Connects is configured with the value
Overwrite
in theAdd by
column.
Service Layout Configuration
Up to four custom integrations can be configured following the structure below.
[
{
"location": "",
"url": "",
"title": "",
"height": ""
}
]
Location
The place where the custom integration should be loaded. Possible values are:
View | Workitem | Home | Hidden |
---|---|---|---|
![]() | ![]() | ![]() | |
The custom integration is displayed in its own view. | The custom integration is only visible on workitems. | The custom integration is only visible on the Home screen. | The custom integration is hidden and always active in the background. |
Only one integration can be used per location. Additional integrations will be ignored.
The page timing information is added automatically in ServiceNow when using a UI page. If you choose the location View
or Home
, there may be an alarm clock symbol displayed at the bottom which cannot be removed.
URL
Where to load the custom integration from. The url can be absolute as well as relative (in case the name of the ui page is used here).
Title
The name of your custom integration. It will be used as a title when displaying the integration.
Height
The height of the iframe the custom integration is loaded into. If this property is not provided, a default value is used.
Make sure to set a value that is equal to or greater than the total height of your custom integration UI Page to avoid additional scrollbars. This value will be ignored if the custom integration is hidden or displayed in a view.