
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
06-11-2020 03:15 AM
Hi there, I'm a newbie that feels like I'm trying to do something beyond my skills - so I apologise in advanced if I'm missing any real basics.
Ultimate Goal: Use a Client Script to trigger a Guided Tour when a Case is in a specific state. Once the Tour has been viewed (tourCompleted) or dismissed (tourDismissed), the tour should not start.
I've used a script from Community Member athm (https://community.servicenow.com/community?id=community_article&sys_id=4d846de3db47130023f4a345ca961...) to achieve the conditional triggering. This works like a charm!
I plan to write an entry to "sys_guided_tour_user_overrides" if the user views or dismisses the tour. For now, I have manually added the entry in the table to test whether I can read from the table:
In order to read from the table, I was originally planning to utilise the Script Include "GTAutoLaunchController". However, rather than messing with a "system" Script, I created my own version of it, to allow me to make it Client Callable and mess around with it. From the Client Script, I am attempting a GlideAjax call. But no matter what I have tried, I believe that it's not triggering the script.
I have added gs.info in various places within the Script Include in an attempt to help me troubleshoot, but nothing appears in the System Log (syslog.list).
The Client Script is as follows:
function onChange(control, oldValue, newValue, isLoading, isTemplate) {
if (isLoading || newValue === '') {
return;
}
//If the current state of the form meets your conditions, launch the tour
if(g_form.getValue('resolution_code') != '0'){
var ga = new GlideAjax('sn_tourbuilder.GTAutoLaunchUtils');
ga.addParam('sysparm_name', 'getOverriddenToursForUser');
ga.addParam('sysparm_userID', g_user.userID);
ga.getXML(checkOveride);
g_form.addInfoMessage(JSON.stringify(ga));
}
function checkOveride(response) {
g_form.addInfoMessage("Response was: " + JSON.stringify(response));
var answer = response.responseXML.documentElement.getAttribute("tourId");
g_form.addInfoMessage(JSON.stringify(answer));
// answer = answer.evalJSON();
// g_form.addInfoMessage(answer);
// for (var i=0 ; i < answer.length ; i++) { //Loop into the array
// g_form.addInfoMessage(answer[i].tourID);
}
// }
top.NOW.guidedToursService.startTour("81996c861b515810e8a7eca13d4bcb6c", 0);
//(sys_id of the guided tour you want to launch, step#)
//Check if the guided tour is dismissed by the user, if so then end the tour.
//If not the system will dispaly a message if the user navigates to a different form
//before the completion of the guided tour.
setInterval(function(){
if(top.NOW.guidedToursService.isDismissed){
top.NOW.guidedToursService.endTour();
clearInterval();
}
},2000);
}
The results from the InfoMessages are as follows:
g_form.addInfoMessage(JSON.stringify(ga));
var GTAutoLaunchUtils = Class.create();
var tourTypes = {
SERVICE_PORTAL: 'service_portal',
PLATFORM: 'platform',
CUSTOM: 'custom_ui'
};
GTAutoLaunchUtils.prototype = Object.extendsObject(global.AbstractAjaxProcessor, {
type: 'GTAutoLaunchUtils'
});
GTAutoLaunchUtils.prototype.tableNames = {
tours: 'sys_embedded_tour_guide',
userOverrides: 'sys_guided_tour_user_overrides'
};
insertOverride = function(tourId, userId) {
var rec = new GlideRecord(this.tableNames.userOverrides);
rec.initialize();
rec.tour = tourId;
rec.user = userId;
rec.disable_autolaunch = true;
rec.insert();
};
removeOverrides = function(page, userId, portal) {
var self = this;
var pages = this.getToursForPage(page, portal);
var ids = pages
.map(function(d) { return d.id; })
;
if (ids.length) {
var rec = new GlideRecord(self.tableNames.userOverrides);
if (userId) {
rec.addQuery('user', '=', userId);
}
rec.addQuery('tour', 'IN', ids.join(','));
rec.deleteMultiple();
return {ids: ids};
} else {
return null;
}
};
overrideTourForUser = function(tourId) {
var currentUser = gs.getUser();
var rec = new GlideRecord(this.tableNames.userOverrides);
rec.addQuery('user', '=', currentUser.getID());
rec.addQuery('tour', '=', tourId);
rec.query();
var exists = rec.next();
var tourRecord = new GlideAggregate(this.tableNames.tours);
tourRecord.addQuery('sys_id', '=', tourId);
tourRecord.query();
if(tourRecord.hasNext() && !exists) {
this._insertOverride(tourId, currentUser.getID());
return {msg: this.messages.done};
} else if(tourRecord.hasNext() && exists && !rec.disable_autolaunch){
rec.disable_autolaunch = true;
rec.update();
return {msg: this.messages.done};
}else {
return null;
}
};
overrideAllToursForUserInPage = function(page, portal) {
var self = this;
var currentUser = gs.getUser();
var userId = currentUser.getID();
var res = this._removeOverrides(page, userId, portal);
if (res) {
res.ids.forEach(function(id) {
self._insertOverride(id, userId);
});
return {msg: self.messages.done};
} else {
return null;
}
};
getOverriddenToursForUser = function() {
gs.info("The script ran");
var userID = this.getParameter('sysparm_userID');
var gr = new GlideRecord("sys_guided_tour_user_overrides");
var data = [];
gr.addQuery('user', userID);
gr.addQuery('disable_autolaunch',true);
gr.query();
gs.info("This is killing me, will this work " + gr.tour);
while(gr.next()) {
gs.info("Results from this script " + gr.tour);
var elem = {tourId: '' + gr.tour};
data.push(elem);
}
return data.toString();
};
I have tried to follow all of the advice in GlideAjax Troubleshooting Guide, I've attempted multiple variations on passing arrays, JSON etc. I've tried different methods of logging, but I believe that gs.info is correct for a Scoped Application.
Ultimately, I'm not convinced that the script is being called (due to the lack of logging). I have run a couple basic Background Scripts (in Global scope) to test whether I can indeed get a result back from the table - and I can:
var gr = new GlideRecord('sys_guided_tour_user_overrides');
gr.addQuery('user','a1fa60be1b6e40d044addac3cc4bcbb9');
gr.addQuery('disable_autolaunch',true);
gr.query();
while(gr.next()){
gs.info("Tour ID: " + gr.tour);
}
Returns:
*** Script: Tour ID: 81996c861b515810e8a7eca13d4bcb6c
var gr = new GlideRecord('sys_guided_tour_user_overrides');
gr.addQuery('user','a1fa60be1b6e40d044addac3cc4bcbb9');
gr.addQuery('disable_autolaunch',true);
gr.query();
var data = [];
while(gr.next()){
var elem = {tourId: '' + gr.tour};
data.push(elem);
}
data.toString();
gs.info(JSON.stringify(data));
Returns:
*** Script: [{"tourId":"81996c861b515810e8a7eca13d4bcb6c"}]
I'm totally at a loss and hope you can help/point me in the right direction.
Thanks!
Solved! Go to Solution.
- Labels:
-
Guided Tours

- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
06-11-2020 03:54 AM
Hi
I have not tested out, but I think, you closed your Prototype definition TOO early (look at my screenshot).
You need to put all the code within the curly braces, to make them belong to the Class. You put all of your functions outside, which will make them "unreachable".
Below, I copied the starting part of an OOB AJAX Script include. There you can see, that all functions are INSIDE.
Additionally, make sure to name your Script Include EXACTLY as you do in the code.
Let me know if that answers your question and mark my answer as correct/helpful.
Have fun & Enjoy ServiceNow
BR
Dirk

- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
06-11-2020 07:15 AM

- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
06-11-2020 08:08 AM
Hi
It seems like you have a few steps that are not necessary. If you want to return only the tour name from multiple guided tour records you can push them into the array with data.push(gr.getDisplayValue(tour)). When returning that value to the client script you convert the array to a string with data.toString(). Once back in your client script you can get the answer back to an array which you can loop through using answer.join(','). It makes sense to create an array of objects if you want to return multiple values from multiple records. Otherwise making a simple array works best.
--David

- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
06-11-2020 07:48 AM
Looks like it this line was screwing it up - whether this was because it is a scoped application, I'm not sure?
var json = new JSON();
Now, I am sure I tried working around that with the scoped version JSON API Documentation:
var json = new global.JSON();
And that still failed. In the end, when I commented the line out, it passed through to the Client Script
🤦♂️

- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
06-11-2020 10:23 AM
Hi
In my answer above, I was focussing on getting the AJAX call on the road.
Now, that I can see you having trouble with the implementation, I am wondering, if you can reach what you want in a much simpler way.
I never fired Guided Tours the way you do here, but I like that approach.
I guess, you want to just find out with your AJAX call is, if there is a record in the [sys_guided_tour_user_overrides] table for the current user and the current guided tour.
If that is the case, I recommend just to create a specific public function in your AJAX Script Include just for that.
I recommend to use the "User" and the "Tour" (Sys_ID of the tour) as parameters for your AJAX function, which in turn should just return true or false.
Returning "true" or "false" is easy to send back and easy to consume.
If you want to return it as String, you can go with "YES" and "NO" as return string and work on that in the client.
Does that make sense?
Let me know, we will make it!
BR
Dirk

- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
06-11-2020 10:55 AM
Hi Dirk, firstly - yes - completely agree. The reason for posting was that I simply could not get the Script Include to fire, and you nailed the issue. Huge thanks for that, I really don't think I'd have found that, as I'd assumed because I'd "borrowed" it from ServiceNow code, it was going to be fine.
What you’re suggesting with the is in effect what I’ve done, yes. My code was at an early stage when I shared it....Because I’m brand new to all of this, I like to get the code working in phases - with the end goal always in mind. That way, I only have one part I need to troubleshoot. 😀
I do prefer your approach of returning True or False though. At the moment I have a bit of a cludge, due to passing the JSON string; no results end up returning 2 chars: “[]” so if answer.length < 3 suffices as a test.
function onChange(control, oldValue, newValue, isLoading, isTemplate) {
if (isLoading || newValue === '') {
return;
}
var tourID = ("81996c861b515810e8a7eca13d4bcb6c");
//If Resolution code changes from "-- None --" check if user has viewed the Tour
if(g_form.getValue('resolution_code') != '0'){
// Pass UserID, TourID to GTAutoLaunchUtils ScriptInclude
var ga = new GlideAjax('sn_tourbuilder.GTAutoLaunchUtils');
ga.addParam('sysparm_name', 'getOverriddenToursForUser');
ga.addParam('sysparm_userID', g_user.userID);
ga.addParam('sysparm_tourID', tourID);
ga.getXML(checkOveride);
}
//If any value is returned, the user has not viewed the Tour
function checkOveride(response) {
var answer = response.responseXML.documentElement.getAttribute("answer");
if (answer.length < 3){
top.NOW.guidedToursService.startTour(tourID, 0);
//(sys_id of the guided tour you want to launch, step#)
//Check if the guided tour is dismissed by the user, if so then end the tour.
//If not - system will show a message if the user navigates to a different form
//before the completion of the guided tour.
var timer = setInterval(function(){
if(top.NOW.guidedToursService.isDismissed){
top.NOW.guidedToursService.endTour();
jslog("Tour Dismissed");
clearInterval(timer);
}
if(top.NOW.guidedToursService.isCompleted){
jslog("Tour Completed");
clearInterval(timer);
}
},2000);
}
}
}
The next challenge is fathoming out how to detect if the user completed the tour. I can’t find any documentation for the line: top.NOW.guidedToursService.isDismissed So I’ll either have to figure if the Event Listeners will help or continue digging for where the original poster found .isDismissed and see if there are other functions like .Completed or .wasCompleted or some such! I have slightly enhanced the original code as they hadn’t given the setInterval a name, so the ClearInterval wasn’t honoured.