- Post History
- Subscribe to RSS Feed
- Mark as New
- Mark as Read
- Bookmark
- Subscribe
- Printer Friendly Page
- Report Inappropriate Content
2 hours ago
Introduction
This article describes how to migrate UI Actions from the Classic Experience to the Next Experience for Demand Management. The migration approach mirrors the Project Workspace pattern and includes Demand Management–specific examples, with real scripts from the Create EAP Epic and Create Baseline UI action migrations.
|
Before You Begin Ensure you are familiar with: |
This article covers four main types of UI actions you may encounter in Demand Management:
- UI actions that work entirely on the server side
- Simple client-side UI actions (e.g. Create EAP Epic)
- UI actions that work on both the client and server side (e.g. Create Baseline)
- Complex client-side UI actions that display pop-up windows (modals) (e.g. Create Baseline)
Type 1 — Server-side only UI actions
These are the simplest to migrate. They contain only server-side code that runs on the ServiceNow server. A typical Demand Management example is the Submit Demand button that transitions a demand record to the submitted state without client-side validation.
Migration Steps
- Open the UI action record.
- Scroll to the Workspace section.
- Enable Format for configurable Workspace.
- Select the display mode:
- Enable Workspace Form Button to show as a button on the Next Experience record page.
- Enable Workspace Form Menu to show as an option under More Actions in the action bar.
- Set any necessary role restrictions in the Requires role field.
- Save your changes.
|
Display Order To control the display order, add an entry in the UX Form Action Layout Item for the demand table under Record Page Action Layout in the Demand Management WS Record Page Action Config UX Form Configuration. |
Type 2 — Client-side only UI actions
These actions contain only client-side code running entirely in the browser, with no server-side script block. They typically perform validation, call a Script Include via GlideAjax, and update the form based on the response. As these scripts run purely in the browser, migration focuses on replacing deprecated APIs with their workspace equivalents.
The real example covered here is the Create EAP Epic UI action on the dmn_demand table. When selected, it validates that the demand has no unsaved changes, then calls a GlideAjax Script Include to create an EAP Epic from the demand record and displays a success or error message.
Migration Steps
- Open the UI action record.
- Scroll to the Workspace section.
- Enable Format for configurable Workspace.
- Select the display mode (Workspace Form Button or Workspace Form Menu).
- Write the new script in the Workspace Client Script (client_script_v2) field — the classic on-click script cannot be used as-is. Key APIs have changed; see the comparison below.
- Set any necessary role restrictions in the Requires role field.
- Save your changes.
|
Display Order To control the display order, add an entry in the UX Form Action Layout Item for the demand table under Record Page Action Layout in the Demand Management WS Record Page Action Config UX Form Configuration. |
Example — Create EAP Epic UI action
This UI action converts a demand record into an EAP Epic. It is purely client-side: it validates the form state, calls a GlideAjax Script Include, and displays success or error messages inline — no server-side script block is involved.
Original Classic Script (onclick field)
|
// Classic onclick script function createEAPEpic() { var loaderUtils = new EAPEntityLoaderUtils; var modalTitle = getMessage('Creating EAP epic'); var modalMessage = getMessage('EAP epic generation is in progress. This may take a few moments.'); loaderUtils.showLoadingModal(modalTitle, modalMessage);
// g_form.modified — global reference, valid in classic context only if (g_form.modified) { loaderUtils.closeLoadingModal(); return g_form.addErrorMessage( getMessage('Save the demand record before creating an EAP Epic.')); }
var ga = new GlideAjax('AjaxEAPEntityCreationFromDemand'); ga.addParam('sysparm_name', 'createEAPEpic'); ga.addParam('demand_sys_id', g_form.getUniqueValue()); ga.getXML(function(response) { var answer = response.responseXML.documentElement.getAttribute('answer'); try { answer = JSON.parse(answer); if (answer == null || !('sys_id' in answer)) { throw Error(); } // sessionStorage used to show a message after page reload sessionStorage.setItem('demandOnLoadMessage', JSON.stringify({ level: 'info', message: ('additional_message' in answer) ? answer['additional_message'] : getMessage(`EAP Epic <a href='...'>${answer['number']}</a> created successfully.`) })); loaderUtils.closeLoadingModal(); g_form.save(); } catch (e) { loaderUtils.closeLoadingModal(); if (answer != null && 'error' in answer) { g_form.addErrorMessage(answer.error); } else { g_form.addErrorMessage(getMessage( 'Something went wrong. Please check the demand details and try again.')); } } }); } |
Migrated Workspace Client Script (client_script_v2 field)
The workspace-compatible version addresses several incompatibilities in the classic script:
- g_form is no longer a global variable — it is passed as a parameter to onClick().
- g_form.modified is not valid in workspace client scripts — replaced with g_form.isUserModified().
- EAPEntityLoaderUtils (which uses classic modal APIs internally) is not available in the workspace — the loading state is communicated instead via g_form.addInfoMessage().
- sessionStorage is not needed to pass a post-reload message — g_form.addInfoMessage() displays feedback directly before calling g_form.save().
- The GlideAjax Script Include name is qualified with the application scope prefix (sn_apw_advanced.AjaxEAPEntityCreationFromDemand) to ensure correct resolution in the workspace execution context.
- ga.getXMLAnswer() is used instead of ga.getXML() to simplify response parsing.
|
// Workspace Client Script (client_script_v2) function onClick(g_form) {
// g_form.modified is not valid in workspace — use g_form.isUserModified() if (g_form.isUserModified()) { g_form.addErrorMessage( getMessage('Save the demand record before creating an EAP Epic.')); return false; }
// Show progress messages inline (EAPEntityLoaderUtils not available in workspace) g_form.addInfoMessage(getMessage('Creating EAP epic')); g_form.addInfoMessage( getMessage('EAP epic generation is in progress. This may take a few moments.'));
var demandSysId = g_form.getUniqueValue();
// Qualify the Script Include with its application scope prefix var ga = new GlideAjax('sn_apw_advanced.AjaxEAPEntityCreationFromDemand'); ga.addParam('sysparm_name', 'createEAPEpic'); ga.addParam('demand_sys_id', demandSysId);
// ga.getXMLAnswer() simplifies response handling vs ga.getXML() ga.getXMLAnswer(function(answer) { var result = null; try { result = JSON.parse(answer); if (result == null || !('sys_id' in result)) { throw new Error('Invalid response'); } // Build an inline link to the new epic var epicLink = '<a href="/now/alignment-workspace/eap/sub/record/' + 'sn_align_core_scrum_epic/' + result.sys_id + '" target="_blank">' + result.number + '</a>';
g_form.clearMessages(); var message = ('additional_message' in result) ? result.additional_message : getMessage('EAP Epic {0} created successfully.').replace('{0}', epicLink); g_form.addInfoMessage(message);
// Brief delay so the user can read the success message before reload setTimeout(function() { g_form.save(); }, 1000);
} catch (e) { g_form.clearMessages(); if (result != null && 'error' in result) { g_form.addErrorMessage(result.error); } else { g_form.addErrorMessage(getMessage( 'Something went wrong. Please check the demand details and try again.' + ' Contact your administrator if the issue persists.')); } } }); return false; } |
Key API Changes — Create EAP Epic
|
Deprecated (Classic) |
Workspace Replacement |
Notes |
|
g_form.modified |
g_form.isUserModified() |
g_form is a parameter, not a global; .modified not valid in workspace. |
|
EAPEntityLoaderUtils |
g_form.addInfoMessage() |
Classic loader utility not available in workspace; use inline messages instead. |
|
sessionStorage.setItem() |
g_form.addInfoMessage() before g_form.save() |
sessionStorage approach not needed; display feedback directly in the workspace form. |
|
ga.getXML(fn) |
ga.getXMLAnswer(fn) |
Simpler callback; answer is the string value, no need to parse responseXML manually. |
|
'AjaxEAPEntityCreationFromDemand' |
'sn_apw_advanced.AjaxEAPEntityCreationFromDemand' |
Qualify Script Include with scope prefix for reliable resolution in workspace context. |
|
g_form.save() immediately |
setTimeout(g_form.save, 1000) |
Short delay allows the user to read the success message before the page reloads. |
Type 3 and 4 — Client + server-side UI actions with pop-up modal
Some UI actions combine all three layers: a client-side script (workspace client script), a server-side script block on the UI action, and a UI Page rendered inside a modal iframe. The Create Baseline UI action on the dmn_demand table is the real Demand Management example of this pattern.
When the user clicks Create Baseline:
- The workspace client script opens a modal dialog using g_modal.showFrame() — this is the client-side layer.
- The user fills in the baseline name and clicks Save inside the modal. The UI Page's processing script (server-side Jelly script) runs on the ServiceNow server to insert the baseline record — this is the server-side layer.
- The UI Page client script posts a message back to the workspace via iframeMsgHelper, triggering the callback in the workspace client script, which reloads the demand form.
This type of action requires changes in three places: the UI action's Workspace Client Script (client_script_v2), the UI Page HTML (to include the iframeMsgHelper script and a hidden form_source input), and the UI Page client script (to use iframeMsgHelper instead of classic dialog APIs). The server-side processing script does not need to change.
Migration Steps
- Open the UI action record.
- Scroll to the Workspace section.
- Enable Format for configurable Workspace.
- Select the display mode — in this case Workspace Form Menu (form_menu_button_v2), so the action appears under More Actions in the workspace action bar.
- Write the new Workspace Client Script (client_script_v2) using g_modal.showFrame() — see below.
- Update the UI Page: include SPMGmodalIframeMsgHelper, add the form_source hidden input, and update button handlers — see UI Page Changes below.
- The server-side script block (processing script in the UI Page) does not require changes.
- Set any necessary role restrictions in the Requires role field.
- Save your changes.
Step 1 — Original classic script (UI action onclick)
The original classic script used GlideModal to render the create_demand_baseline UI page as a dialog. The classic script block on the UI action was:
|
// Classic UI Action — script field (onclick: createBaselineFromDemand()) function createBaselineFromDemand() { var sysId = g_form.getUniqueValue(); var tableName = g_form.getTableName();
// GlideModal is not supported in workspace client scripts var gDialog = new GlideModal('create_demand_baseline'); gDialog.setPreference('sysparm_sysID', sysId + ''); gDialog.setPreference('sysparm_table', tableName + ''); gDialog.setPreference('sysparm_task_id', sysId); gDialog.setTitle(getMessage('Create Baseline')); gDialog.setPreference('focusTrap', true); gDialog.setWidth(500); gDialog.render(); } |
Step 2 — Migrated workspace client script (client_script_v2)
The workspace-compatible script is written in the client_script_v2 field. It replaces GlideModal with g_modal.showFrame() and passes parameters as URL query string values instead of GlideModal preferences. The key addition is sysparm_form_source=workspace, which tells the UI Page it is running inside the workspace modal context.
|
// Workspace Client Script — client_script_v2 field on the UI Action function onClick(g_form) { var sysId = g_form.getUniqueValue(); var tableName = g_form.getTableName();
// Parameters are passed as URL query string (not as GlideModal preferences) // sysparm_form_source=workspace signals the UI Page to use workspace-compatible APIs var url = '/sn_align_ws_create_demand_baseline_v2.do?' + 'sysparm_task_id=' + encodeURIComponent(sysId) + '&sysparm_table=' + encodeURIComponent(tableName) + '&sysparm_has_parent=true' + '&sysparm_form_source=workspace' + '&sysparm_stack=no';
g_modal.showFrame({ title: getMessage('Create Baseline'), url: url, size: 'md', height: '250px', width: '500px', // autoCloseOn: 'URL_CHANGED', // uncomment if auto-close on redirect is preferred callback: function(ret, data) { if (data && data.actionType == 'ok') { if (data.status == 'success') { // Reload the demand record page to reflect the new baseline g_form.save(); } } // data.actionType == 'cancel': user dismissed dialog — no action needed } }); } |
Step 3 — Required UI page changes (create_demand_baseline_v2)
The UI page loaded inside the modal must also be updated. The classic approach of calling sendEvent() or GlideDialogWindow.get().destroy() does not work inside a workspace iframe. The page must use the iframeMsgHelper API (provided by the SPMGmodalIframeMsgHelper UI script) to communicate back to the workspace callback.
3a. Include SPMGmodalIframeMsgHelper in the UI page HTML
Add the following tag inside the <g:ui_form> element in the UI page HTML. This makes the iframeMsgHelper object available in the UI page client script when it runs inside the workspace iframe:
|
<!-- Inside <g:ui_form> in the UI Page Jelly HTML --> <g:requires name="SPMGmodalIframeMsgHelper.jsdbx" includes="true"/> |
3b. Add a hidden input to capture sysparm_form_source
The UI page client script needs to know whether it is running inside the workspace or the classic UI. Add a hidden input that captures the sysparm_form_source URL parameter passed by the workspace client script:
|
<!-- Hidden input in the UI Page HTML — captures the form source context --> <input type="hidden" id="form_source" name="form_source" value="${sysparm_form_source}"/> |
3c. Update the Cancel button handler
Replace GlideDialogWindow.get().destroy() with iframeMsgHelper.cancel() when running in the workspace. The iframeMsgHelper.cancel() call closes the modal and triggers the callback in g_modal.showFrame() with actionType: 'cancel':
|
// UI Page client script — Cancel button handler function onCancel() { // has_parent check is kept for classic non-workspace contexts if (gel('has_parent').value !== 'true') { GlideDialogWindow.get().destroy(); } if (gel('form_source').value == 'workspace') { // Closes the workspace modal and triggers the g_modal.showFrame callback iframeMsgHelper.cancel({ actionType: 'cancel' }); } sendEvent('modal:close'); return false; } |
3d. Update the Save (OK) button handler
Replace sendEvent(msg) with iframeMsgHelper.confirm() when running in the workspace. The data object passed to confirm() is forwarded directly to the callback registered in g_modal.showFrame() — this is how the workspace client script knows to call g_form.save():
|
// UI Page client script — Save (OK) button handler function actionOK() { var name = trim(gel('baseline_name').value); if ('' == name) { alert(getMessage('Enter the baseline name')); var e = gel('baseline_name'); if (e) e.focus(); return false; } showLoadMask(); var msg = gel('message').value; if (msg) { if (gel('form_source').value == 'workspace') { // Post success data to the g_modal.showFrame callback in the workspace iframeMsgHelper.confirm({ actionType: 'ok', status: 'success', message: msg }); } else { sendEvent(msg); // classic context only } sendEvent('modal:close'); } return true; }
// Classic context helper — not used in workspace function sendEvent(event) { parent.postMessage(event, '*'); } |
Complete updated UI page client script
Below is the full client script for create_demand_baseline_v2 after migration, exactly as it appears in the UI page record:
|
function onCancel() { if (gel('has_parent').value !== 'true') { GlideDialogWindow.get().destroy(); } if (gel('form_source').value == 'workspace') { iframeMsgHelper.cancel({ actionType: 'cancel' }); } sendEvent('modal:close'); return false; }
function actionOK() { var e; var name = trim(gel('baseline_name').value); if ('' == name) { alert(getMessage('Enter the baseline name')); e = gel('baseline_name'); if (e) e.focus(); return false; } showLoadMask(); var msg = gel('message').value; if (msg) { if (gel('form_source').value == 'workspace') { iframeMsgHelper.confirm({ actionType: 'ok', status: 'success', message: msg }); } else { sendEvent(msg); } sendEvent('modal:close'); } return true; }
function showLoadMask() { var loadMask = document.getElementsByClassName('load_mask_container')[0]; loadMask.style.display = 'flex'; }
function sendEvent(event) { parent.postMessage(event, '*'); } |
End-to-End Flow — How All Three Layers Work Together
Understanding the full execution chain is important when debugging this pattern:
- User selects Create Baseline on the demand record page in the workspace.
- The workspace client script (client_script_v2) calls g_modal.showFrame(), opens sn_align_ws_create_demand_baseline_v2.do in a modal iframe. The URL includes sysparm_form_source=workspace and sysparm_task_id.
- The UI page loads inside the iframe. The SPMGmodalIframeMsgHelper script is included, making iframeMsgHelper available. The hidden form_source input is set to workspace.
- The user enters a baseline name and description, then selects Save. The UI page form submits to its processing script (server-side Jelly). This script creates the dmn_demand_baseline_header record, calls the BaselineAPI, and redirects with a sysparm_message query parameter.
- On redirect, the UI page client script reads the message value from the hidden input. Because form_source is workspace, it calls iframeMsgHelper.confirm({ actionType: 'ok', status: 'success', message: msg }). Internally this uses window.parent.postMessage.
- The workspace receives the message and invokes the callback in g_modal.showFrame(). data.actionType == 'ok' and data.status == 'success', so the callback calls g_form.save() to reload the demand record page.
- If the user selects Cancel at any point, iframeMsgHelper.cancel() is called. The modal closes and the callback fires with data.actionType == 'cancel' — no reload occurs.
|
When to use a Declarative Action instead If you cannot find workspace-compatible modal APIs for your use case, or the UI page changes are too extensive, consider creating a fresh Declarative Action. Refer to: Declarative Actions in ServiceNow: The COMPLETE Guide |
Full API Change Reference
The following table summarises all API changes relevant to Demand Management UI action migrations:
|
Deprecated (Classic) |
Workspace Replacement |
Notes |
|
g_form.modified |
g_form.isUserModified() |
Returns boolean; checks unsaved user edits |
|
g_form.modifiedFields |
g_form.$private.getField(n).value vs .originalValue |
Build hasFieldModified() helper |
|
gsftSubmit() |
g_form.submit(actionName) |
actionName from g_form.getActionName() |
|
GlideModal / GlideDialogWindow |
g_modal.showFrame({}) |
Passes URL; params via query string |
|
GlideModal preferences |
URL query string parameters |
e.g. sysparm_task_id=... in the URL |
|
window.location.reload() |
g_form.reload() or g_form.save() |
From workspace callback after iframe confirm |
|
GlideDialogWindow.destroy() |
iframeMsgHelper.cancel() |
In UI page client script; closes modal |
|
sendEvent(msg) |
iframeMsgHelper.confirm({...}) |
Posts data back to g_modal.showFrame callback |
|
GlideAjax |
GlideAjax (still supported) |
Verify Script Include scope accessibility |
Troubleshooting Tips
- UI action not visible in workspace: Confirm that Format for configurable Workspace is enabled and either Workspace Form Button or Workspace Form Menu is selected.
- iframeMsgHelper is undefined in the UI Page: Ensure the SPMGmodalIframeMsgHelper.jsdbx UI script is included via <g:requires> in the UI page HTML.
- Callback never fires after selecting Save: Verify the UI Page client script is calling iframeMsgHelper.confirm() or iframeMsgHelper.cancel() — not sendEvent() or window.parent.postMessage() directly, as the message format must match what the workspace expects.
- Modal opens but shows a blank page or 404: Check that the UI Page endpoint name matches the URL used in g_modal.showFrame(), including the application scope prefix (e.g. sn_align_ws_create_demand_baseline_v2.do).
- g_form.save() in callback has no visible effect: Confirm the demand record page is still active. If the modal navigated away, use g_form.reload() instead.
- GlideAjax 404 in workspace: Verify the Script Include is in the correct application scope and that Accessible from is set appropriately.
- Script works in Classic but not workspace: Check for any usage of GlideDialogWindow, GlideModal, window, or document APIs — none are available in workspace client scripts.
- Button display order is incorrect: Verify the UX Form Action Layout Item entry exists for the correct table and is linked to the right Record Page Action Layout.
For additional guidance, refer to the ServiceNow Developer documentation on Declarative Actions and Workspace UI Builder.

