Introduction
If you’ve ever tried to trigger a workspace modal from a list action, you’ve probably hit some quirks, especially when using reference fields. For example, calling g_modal.showFields()
with a reference field works fine on a form, but breaks when launched from a list.
On top of that, if you’re trying to build a nice modal experience in a UI Page—something more modern than alert()
or confirm()
—you’ll quickly notice the default browser prompts aren’t great and the workspace modal APIs aren’t usable inside Jelly.
This post walks through both of these issues and shows how to fix them.
Problem 1: Reference fields don’t work from list modals
Here’s a basic g_modal.showFields()
call with a reference field, in a Workspace UI action / Action Assignment1.
This is in an Action Assignment of type List Action, Implemented as Client Scipt, and this script would go in the Client Script setion
var fields = [
{
type: 'reference',
name: 'caller_id',
label: getMessage('Who is the user?'),
mandatory: true,
reference: 'sys_user',
referringTable: 'incident',
referringRecordId: g_form.getUniqueValue(),
value: g_form.getValue('caller_id'),
displayValue: g_form.getDisplayValue('caller_id')
}
];
g_modal.showFields({
title: "Select a user",
fields: fields,
size: 'lg'
}).then(function(fieldValues) {
g_form.setValue('caller_id', fieldValues.updatedFields[0].value);
g_form.save();
});
The problem is that referringTable
, referringRecordId
, and name
all assume the context of a form. If you trigger this from a list button, you’re missing the right record context, and it fails.
Solution: use a UI Page inside a modal
So, instead of using g_modal.showFields()
, you can use g_modal.showFrame()
to load a custom UI Page. That page gives you full control over inputs, including reference fields.
Step 1: create a UI page
To keep things simple, let’s start with native alert()
and confirm()
.
Create a UI page, called in this example select_author
, and add this to the HTML part:
<?xml version="1.0" encoding="utf-8" ?>
<j:jelly trim="false" xmlns:j="jelly:core" xmlns:g="glide" xmlns:j2="null" xmlns:g2="null">
<g:ui_reference name="author" id="author" table="x_1588981_demo_authors" query="active=true" />
<g:dialog_buttons_ok_cancel ok="return actionOK();" cancel="return cancel();" />
</j:jelly>
And then this to the Client Script:
function actionOK() {
// Get the value from the reference field with ID 'author'
var authorId = gel('author').value;
// If no value is selected, show an alert and stop execution
if (!authorId) {
alert("Please select an author");
return false;
}
// Ask the user for confirmation before proceeding
if (confirm("Are you sure you want to proceed?")) {
// In a real version, this would send the selected authorId back to the parent modal
// For now, we just show a confirmation message
alert("Confirmed with author: " + authorId);
}
// Prevent default form submission behavior
return false;
}
function cancel() {
// Show a cancellation message and prevent default behavior
alert("Action cancelled");
return false;
}
Your UI Page will also need a matching UI Page ACL to allow access. You will be asked when saving it the first time, but in case you want more information you can refer to ACL rule types documentation2 and the article “How to create ACL for UI page” from theServiceNow Community3 for details.
Step 2: Call the page from Workspace
On the UI Action / Action Assignment, replace the Client Script with the one below. It use the g_modal.showFrame()
that use the UI page we created previously.
function onClick() {
// Open a UI Page inside a modal using g_modal.showFrame
g_modal.showFrame({
url: '/x_1588981_demo_select_author.do', // UI Page to load. Note: the .do suffix is automatically added to the UI Page name
title: "Select an author", // Modal title
size: 'xl', // Modal width size
autoCloseOn: 'URL_CHANGED', // Automatically close modal if iframe URL changes
callback: function (ret, data) {
// If the user confirmed the modal
if (ret) {
// Log the returned data (e.g., selected authorId)
console.log(data);
}
}
});
}
Concerning the part autoCloseOn: 'URL_CHANGED'
: the goal is to closes the modal automatically when the iframe navigates. This works nicely with the close pattern:
function close() {
window.location.href = window.location.href + '&sysparm_next_pg=true';
}
This triggers the URL_CHANGED
condition and closes the modal from within the iframe.
Problem 2: Returning values and closing the modal
If you try the button now, the UI page open in a modal, but if you select an author and click OK, nothing happens. This is because it lacks a mechanism to return values to the calling script.
For this, the iframeHelper
come to the rescue. This utility solves the communication with the Actoin Assignment using window.postMessage()
to confirm or cancel modal interactions4.
iframeHelper: a postMessage helper for modals
Here’s a UI Script (iframeHelper
) that includes confirm
, cancel
methods. It is from the snpro blog4, but I copied it here for convenience.
Simply create a new UI script:
- Script name:
iframeHelper
- UI Script: All" types.
- Script: copy/paste the code below
/**
* Script from
* "Workspace Modals - Working with UI Pages - SNPro.dev"
* - https://snpro.dev/2023/11/20/workspace-modals-working-with-ui-pages/
* (c) Callum Ridley
*
*/
var iframeHelper = (function() {
function createPayload(action, modalId, data) {
return {
messageType: 'IFRAME_MODAL_MESSAGE_TYPE',
modalAction: action,
modalId: modalId,
data: (data ? data : {})
};
}
function pm(window, payload) {
if (window.parent === window) {
console.warn('Parent is missing. Is this called inside an iFrame?');
return;
}
window.parent.postMessage(payload, location.origin);
}
function IFrameHelper(window) {
this.window = window;
this.src = location.href;
this.messageHandler = this.messageHandler.bind(this);
this.window.addEventListener('message', this.messageHandler);
}
IFrameHelper.prototype.messageHandler = function(e) {
if (e.data.messageType !== 'IFRAME_MODAL_MESSAGE_TYPE' || e.data.modalAction !== 'IFRAME_MODAL_ACTION_INIT') {
return;
}
this.modalId = e.data.modalId;
};
IFrameHelper.prototype.confirm = function(data) {
var payload = createPayload('IFRAME_MODAL_ACTION_CONFIRMED', this.modalId, data);
pm(this.window, payload);
};
IFrameHelper.prototype.cancel = function(data) {
var payload = createPayload('IFRAME_MODAL_ACTION_CANCELED', this.modalId, data);
pm(this.window, payload);
};
IFrameHelper.prototype.hideCloseButton = function() {
var iframeRootNode = this.window.frameElement?.getRootNode();
if (iframeRootNode) {
var scriptedModal = iframeRootNode.querySelector('sn-scripted-modal');
if (scriptedModal) {
var nowModal = scriptedModal.shadowRoot?.querySelector('now-modal');
if (nowModal) {
var modalFooter = nowModal.shadowRoot?.querySelector('.now-modal-footer');
if (modalFooter) {
modalFooter.style = 'display: none;';
}
}
}
}
}
IFrameHelper.prototype.autoResize = function(add) {
var iframeRootNode = this.window.frameElement?.getRootNode();
var slotEl = iframeRootNode.querySelector('.slot-content');
var body = document.body;
var html = document.documentElement;
var height = Math.max(body.scrollHeight, body.offsetHeight,
html.clientHeight, html.scrollHeight, html.offsetHeight);
slotEl.style.height = (height + 40) + 'px';
}
return new IFrameHelper(window);
}());
You will get a warning message like this one, but you can safely click “OK”:

Update the UI Page
After creating the helper, update the the UI Page, by adding this line in the HTML part, in the jelly:
<g:requires name="x_1588981_demo.iframeHelper.jsdbx"></g:requires>
Then adapt the Client Script part as follow:
// When the DOM is fully loaded, auto-resize the modal using the iframeHelper
$j(document).ready(function() {
iframeHelper.autoResize();
});
function actionOK() {
var authorId = gel('author').value;
if (!authorId) {
alert("Please select an author");
return false;
}
if (confirm("Are you sure you want to proceed?")) {
// Send the selected authorId back to the parent modal using iframeHelper
iframeHelper.confirm({
authorId: authorId
});
}
return false;
}
function cancel() {
// Cancel the modal and close it without returning data
iframeHelper.cancel();
return false;
}
As you might have noticed, we now:
- use the
autoresize
at the start of the Client Script, which resize the modal dynamically; - use
cancel
to close the modal (and return control to the calling button). - use
confirm
to send back the data to the calling button;
As a reminder, the object you pass to iframeHelper.confirm() (e.g., { authorId }) is returned directly to the callback function in your Action Assignment. as data
in this example:
callback: function (ret, data) {
// If the user confirmed the modal
if (ret) {
// Log the returned data (e.g., selected authorId)
console.log(data);
}
}
This data is passed directly to your callback. If you later use it in server-side code (like GlideAjax), be sure to validate it. Never trust client input blindly.
Problem 3: Building custom alert and confirm modals in UI Pages
At this point, you probably noticed that the alert and confirm message box are the standard one. Those native alert()
and confirm()
calls are functional but clunky. They don’t match the workspace UX, and you can’t use workspace modals directly in UI Pages.

Here’s how to build nicer modals with inline HTML and JavaScript.
Add the following code in the HTML part of the UI page:
<!-- Alert modal -->
<div id="alertModal" style="display:none; position:fixed; left:0; top:0; width:100vw; height:100vh; background:rgba(0,0,0,0.4); z-index:9999;">
<div style="background:white; padding:2em; margin:10% auto; width:300px; border-radius:10px; box-shadow:0 4px 16px #333;">
<div id="modalMessage"></div>
<button onclick="hideAlertModal()">OK</button>
</div>
</div>
<!-- Confirm modal -->
<div id="confirmModal" style="display:none; position:fixed; left:0; top:0; width:100vw; height:100vh; background:rgba(0,0,0,0.4); z-index:9999;">
<div style="background:white; padding:2em; margin:10% auto; width:320px; border-radius:10px; box-shadow:0 4px 16px #333;">
<div id="confirmMessage"></div>
<button id="confirmOKBtn">OK</button>
<button id="confirmCancelBtn">Cancel</button>
</div>
</div>
These modal blocks use inline styles for layout and appearance. You can adjust the width or make them dynamic with JS if needed, but the fixed size works well in most cases.
Then add those functions in the Client Script
// Show the alert modal with a given message
function showAlertModal(message) {
// Set the alert message content
document.getElementById('modalMessage').innerText = message;
// Make the alert modal visible
document.getElementById('alertModal').style.display = 'block';
}
// Hide the alert modal
function hideAlertModal() {
document.getElementById('alertModal').style.display = 'none';
}
// Show a custom confirm modal with a message and a callback for the user's choice
function showConfirmModal(message, callback) {
var modal = document.getElementById('confirmModal');
// Set the confirm message content
document.getElementById('confirmMessage').innerText = message;
// Make the confirm modal visible
modal.style.display = 'block';
// Internal function to clean up after user makes a choice
function cleanUp(result) {
modal.style.display = 'none';
// Remove previous click handlers to avoid stacking
document.getElementById('confirmOKBtn').onclick = null;
document.getElementById('confirmCancelBtn').onclick = null;
// Run the callback with the user's choice (true or false)
if (typeof callback === 'function') callback(result);
}
// OK button confirms the action
document.getElementById('confirmOKBtn').onclick = function() { cleanUp(true); };
// Cancel button cancels the action
document.getElementById('confirmCancelBtn').onclick = function() { cleanUp(false); };
}
And replace the alert
and confirm
calls with the new functions:
if (!authorId) {
//alert("Please select an author");
showAlertModal("Please select an author");
return false;
}
/*if (confirm("Are you sure you want to proceed?")) {
// Send the selected authorId back to the parent modal using iframeHelper
iframeHelper.confirm({
authorId: authorId
});
}
*/
showConfirmModal("Are you sure you want to continue?", function(confirmed) {
if (confirmed) {
iframeHelper.confirm({
authorId: authorId
});
} else {
// Do nothing
}
});
Et voilà: you now have a nice message:

Complete, working example of the UI page
Here is the complete and working example. This is this part you may want to copy/paste and adapt.
HTML
<?xml version="1.0" encoding="utf-8" ?>
<j:jelly trim="false" xmlns:j="jelly:core" xmlns:g="glide" xmlns:j2="null" xmlns:g2="null">
<g:requires name="x_1588981_demo.iframeHelper.jsdbx"></g:requires>
<g:ui_reference name="author" id="author" table="x_1588981_demo_authors" query="active=true" />
<g:dialog_buttons_ok_cancel ok="return actionOK();" cancel="return cancel();" />
<!-- Alert modal -->
<div id="alertModal" style="display:none; position:fixed; left:0; top:0; width:100vw; height:100vh; background:rgba(0,0,0,0.4); z-index:9999;">
<div style="background:white; padding:2em; margin:10% auto; width:300px; border-radius:10px; box-shadow:0 4px 16px #333;">
<div id="modalMessage"></div>
<button onclick="hideAlertModal()">OK</button>
</div>
</div>
<!-- Confirm modal -->
<div id="confirmModal" style="display:none; position:fixed; left:0; top:0; width:100vw; height:100vh; background:rgba(0,0,0,0.4); z-index:9999;">
<div style="background:white; padding:2em; margin:10% auto; width:320px; border-radius:10px; box-shadow:0 4px 16px #333;">
<div id="confirmMessage"></div>
<button id="confirmOKBtn">OK</button>
<button id="confirmCancelBtn">Cancel</button>
</div>
</div>
</j:jelly>
client script
// When the DOM is fully loaded, auto-resize the modal using the iframeHelper
$j(document).ready(function() {
iframeHelper.autoResize();
});
function actionOK() {
var authorId = gel('author').value;
if (!authorId) {
showAlertModal("Please select an author");
return false;
}
showConfirmModal("Are you sure you want to continue?", function(confirmed) {
if (confirmed) {
iframeHelper.confirm({
authorId: authorId
});
} else {
// Do nothing
}
});
return false;
}
function cancel() {
// Cancel the modal and close it without returning data
iframeHelper.cancel();
return false;
}
// Show the alert modal with a given message
function showAlertModal(message) {
// Set the alert message content
document.getElementById('modalMessage').innerText = message;
// Make the alert modal visible
document.getElementById('alertModal').style.display = 'block';
}
// Hide the alert modal
function hideAlertModal() {
document.getElementById('alertModal').style.display = 'none';
}
// Show a custom confirm modal with a message and a callback for the user's choice
function showConfirmModal(message, callback) {
var modal = document.getElementById('confirmModal');
// Set the confirm message content
document.getElementById('confirmMessage').innerText = message;
// Make the confirm modal visible
modal.style.display = 'block';
// Internal function to clean up after user makes a choice
function cleanUp(result) {
modal.style.display = 'none';
// Remove previous click handlers to avoid stacking
document.getElementById('confirmOKBtn').onclick = null;
document.getElementById('confirmCancelBtn').onclick = null;
// Run the callback with the user's choice (true or false)
if (typeof callback === 'function') callback(result);
}
// OK button confirms the action
document.getElementById('confirmOKBtn').onclick = function() {
cleanUp(true);
};
// Cancel button cancels the action
document.getElementById('confirmCancelBtn').onclick = function() {
cleanUp(false);
};
}
Final Notes
- The UI Page uses
iframeHelper.autoResize()
to dynamically adjust height. - This pattern works well for modal workflows launched from Workspace list buttons5.
- It’s a reliable but not officially documented pattern—test thoroughly.
Based on implementation in declarative action
assign_to_apartement
, which filters selected records and passes them via GlideAjax. ↩︎