PrasadShelar
Tera Expert

Hello Community,

 

ServiceNow Workspaces are designed for fast, focused work, but many use cases still require custom actions and guided user interaction. A common need is to let users click a button, select records from a list, and perform an action - without leaving the Workspace.

 

In this article, we’ll demonstrate how a UI Action, UI Page, and GlideModal work together in Service Operations Workspace to add a button on the Incident form, display a table list of Task Templates (sys_template) in a popup, and create corresponding Task records based on user selection.

Screenshot 2026-01-15 at 10.10.57 PM.png

 

Requirement

* Add a Route button (UI Action – `sys_ui_action`) on the Incident (`incident`) form in Service Operations Workspace.
* When the user clicks the button, a GlideModal opens a UI Page (`sys_ui_page`) that displays a searchable, multi-select list of Task Profiles from the Templates table (`sys_template`) where the table is `task`. 
* The user selects one or more templates and commits the action. 
* For each selected template, the system creates a child Task record (`task`) linked to the same Incident using the parent field, without changing the Incident assignment. 
* The Incident context (sys_id) is passed to the server-side logic so that workflows, flows, or integrations can be triggered after task creation.

Screenshot 2026-01-15 at 9.11.37 PM.png

 

Core ServiceNow Components

To implement this functionality, you must understand three foundational elements:

  • UI Action: A button or link that triggers a script. In a Workspace, it acts as the client-side entry point to launch modals or process data on the form.

  • UI Page: A custom interface built with HTML, Jelly, and Client Scripts. It is used here to create the modal dialog that displays the list of templates.

  • Script Include: A server-side script library. When set to Client Callable, it allows the UI Page to communicate with the database to create records or perform complex logic.

Screenshot 2026-01-15 at 9.17.43 PM.png

 

Step 1: UI Action - Create the Button on record form on Workspace

To add the Route button to the Incident record in the Workspace:

  1. Navigate to System UI > UI Actions (sys_ui_action) and create a new record for the Incident(incident) table.

    • Name: Route //provide relavant name
    • Action Name: openRouteModal //Must be unique
    • onClick: openRouteModal(); //This function will execute when user click on button
    • Client: True
    • Active: True
    • List v2 Compatible: True
    • Form Button: True
    • Workspace Form Button: True
  2. Enable the Client checkbox and set Format for Configurable Workspace to true.

  3. In the Client Script V2 field, use the g_mdodal API to display your UI Page:

  4. Workspace Client Script - 

function openRouteModal() {
    var incidentId = g_form.getUniqueValue();

    g_modal.showFrame({
        title: 'Select Task Profiles',
        url: '/route_tasks_modal.do?sysparm_parent_id=' + incidentId,
        size: 'lg',
        height: '600px',
        callback: function(retVal) {
            // Refreshes the tasks related list once modal is closed
            g_form.refreshRelatedLists();
        }
    });
}​

 

Step 2: Script Include - Server-Side Processing

The Script Include performs the database operations to generate the child tasks from the selected templates. Navigate to System Definition > Script Includes

  • Name: u_TaskProfileAjax

  • API Name: global.u_TaskProfileAjax

  • Glide AJAX enabled : True (Essential for UI Page access)

  • Active: True

  • Script:
var u_TaskProfileAjax = Class.create();
u_TaskProfileAjax.prototype = Object.extendsObject(AbstractAjaxProcessor, {

    // Global variables for the class
    LOG_PREFIX: "[u_TaskProfileAjax]: ",
    TABLE_INCIDENT_TASK: "incident_task",
    TABLE_TEMPLATE: "sys_template",
    EVENT_NAME: "incident.route.tasks.created",

    /**
     * Entry point for GlideAjax
     */
    createTasksFromTemplates: function() {
        var params = {
            parentId: this.getParameter('sysparm_parent_id'),
            templateIds: this.getParameter('sysparm_template_ids')
        };

        this._logInfo("Process started for Parent: " + params.parentId);

        if (!this._validateParams(params)) {
            return "error_invalid_params";
        }

        return this._processTemplateCreation(params.parentId, params.templateIds.split(','));
    },

    /**
     * Internal logic handler to process record creation
     */
    _processTemplateCreation: function(parentId, templateIds) {
        var createdTaskIds = [];
        var successCount = 0;

        for (var i = 0; i < templateIds.length; i++) {
            var templateId = templateIds[i];
            var templateGr = new GlideRecord(this.TABLE_TEMPLATE);

            if (templateGr.get(templateId)) {
                var targetTable = templateGr.getValue('table') || 'task';
                var newTaskGr = new GlideRecord(targetTable);
                
                newTaskGr.initialize();

                // Apply Template logic
                var template = GlideTemplate.get(templateGr.getUniqueValue());
                if (template) {
                    template.apply(newTaskGr);
                }

                // Best Practice: Force Context Relationship
                newTaskGr.parent = parentId;
                if (targetTable === this.TABLE_INCIDENT_TASK) {
                    newTaskGr.incident = parentId;
                }

                var newSysId = newTaskGr.insert();
                if (newSysId) {
                    successCount++;
                    createdTaskIds.push(newSysId);
                    this._logInfo("Created " + targetTable + " : " + newSysId);
                } else {
                    this._logError("Failed to insert record for template: " + templateId);
                }
            } else {
                this._logError("Template not found for ID: " + templateId);
            }
        }

        // Trigger integrations/workflows via Event Queue
        if (createdTaskIds.length > 0) {
            this._triggerDownstreamLogic(parentId, createdTaskIds);
        }

        return successCount.toString();
    },

    /**
     * Validates input parameters
     */
    _validateParams: function(params) {
        if (!params.parentId || !params.templateIds) {
            this._logError("Validation failed. Missing Parent ID or Template IDs.");
            return false;
        }
        return true;
    },

    /**
     * Fires event to trigger Flows, Notifications, or Integrations
     */
    _triggerDownstreamLogic: function(parentId, taskIds) {
        var incGr = new GlideRecord('incident');
        if (incGr.get(parentId)) {
            gs.eventQueue(this.EVENT_NAME, incGr, taskIds.join(","), gs.getUserName());
            this._logInfo("Event " + this.EVENT_NAME + " queued.");
        }
    },

    /**
     * Standardized Logging
     */
    _logInfo: function(msg) {
        gs.info(this.LOG_PREFIX + msg);
    },

    _logError: function(msg) {
        gs.error(this.LOG_PREFIX + msg);
    },

    type: 'u_TaskProfileAjax'
});

 

Step 3: UI Page - Create the Modal Interface

The UI Page acts as the bridge between the Workspace and the server. It displays the list of templates and captures the user's selection. Navigate to System UI > UI Pages

  • Name: route_tasks_modal

  • Direct: false

  • Category: general

  • HTML:
    This section renders an iframe containing a filtered list of templates and a "Commit" button.
<?xml version="1.0" encoding="utf-8" ?>
<j:jelly trim="false" xmlns:j="jelly:core" xmlns:g="glide">
  <g:ui_form onsubmit="return false;">
    Parent : <input id="parent_incident_id" value="${sysparm_parent_id}" />
<!--
    <div style="padding: 10px; font-family: sans-serif;">
	<div id="parent_incident_id" value="${sysparm_parent_id}" />
      <p><strong>Step 1:</strong> Select profiles from the list.</p>
      <p><strong>Step 2:</strong> Review selection and click <strong>Create Tasks</strong>.</p>
    </div>
-->
    <div id="targetList">
      <iframe
        id="template_iframe"
        src="sys_template_list.do?sysparm_query=table%3Dtask%5Eactive%3Dtrue&amp;sysparm_view=default&amp;sysparm_clear_stack=true"
        style="min-height:400px; width:100%; border:1px solid #ddd;"
        frameborder="0">
      </iframe>
    </div>

    <div id="selection_summary" style="padding: 10px; background: #f4f4f4; border: 1px solid #ddd; border-top: none; font-family: monospace; font-size: 11px;">
        <strong>Selected Sys_IDs:</strong> 
        <div id="display_ids" style="color: #005eda; word-break: break-all; margin-top: 5px;">None</div>
    </div>

    <footer class="modal-footer" style="margin-top: 15px; text-align: right;">
     <!-- <button class="btn btn-default" type="button" onclick="handleWorkspaceClose()">Cancel</button> -->
      <button class="btn btn-primary" type="button" id="submit_button" onclick="processIframeSelection()">Commit</button>
    </footer>
  </g:ui_form>
</j:jelly>

 

  • Client Script:
    This script handles the selection logic and calls the Script Include via GlideAjax.
(function() {
    var LOG_PREFIX = "[u_TaskProfileAjax_UI]: ";
    var SCRIPT_INCLUDE = "u_TaskProfileAjax";
    var IFRAME_ID = "template_iframe";

    function _log(msg, isError) {
        var fullMsg = LOG_PREFIX + msg;
        if (isError) console.error(fullMsg); else console.info(fullMsg);
    }

    setInterval(function() {
        var ids = _getSelectedTemplateIds();
        var display = document.getElementById('display_ids');
        if (display) {
            display.innerHTML = ids.length > 0 ? ids.join(', ') : 'None';
        }
    }, 1000);

    /**
     * CLOSURE LOGIC: Optimized for Workspace
     */
    window.handleWorkspaceClose = function() {
        _log("Closing modal window.");
        
        // 1. Try modern Workspace Modal API first
        if (window.top.g_modal && typeof window.top.g_modal.destroy === 'function') {
            window.top.g_modal.destroy();
        } 
        // 2. Try the iframe message approach
        else if (window.parent && window.parent.postMessage) {
            window.parent.postMessage({ msg: 'ui_action_close_modal' }, '*');
        }
        // 3. Fallback for Classic UI
        else if (typeof GlideModalWindow !== 'undefined' && GlideModalWindow.get()) {
            GlideModalWindow.get().destroy();
        }
    };

    function _getSelectedTemplateIds() {
        var iframe = document.getElementById(IFRAME_ID);
        if (!iframe || !iframe.contentWindow) return [];
        var w = iframe.contentWindow;
        try {
            if (w.g_list) return (w.g_list.getChecked() || '').split(',').filter(Boolean);
            var boxes = iframe.contentDocument.querySelectorAll('input[name="cb_sys_template"]:checked');
            var ids = [];
            for (var i = 0; i < boxes.length; i++) { ids.push(boxes[i].value); }
            return ids;
        } catch (e) { return []; }
    }

    /**
     * USER FRIENDLY SUBMISSION
     */
    window.processIframeSelection = function() {
        var parentId = document.getElementById('parent_incident_id').value;
        var selectedIds = _getSelectedTemplateIds();

        if (!selectedIds.length) {
            alert('Please select at least one Task Profile.');
            return;
        }

        // --- STEP 1: UI Feedback ---
        var btn = document.getElementById('submit_button');
        btn.disabled = true;
        btn.className = "btn btn-disabled"; // Visual change
        btn.innerHTML = '<span class="icon-loading" style="margin-right:5px"></span> Creating Tasks...';

        _log("Sending GlideAjax request...");

        // --- STEP 2: Server Call ---
        var ga = new GlideAjax(SCRIPT_INCLUDE);
        ga.addParam('sysparm_name', 'createTasksFromTemplates');
        ga.addParam('sysparm_parent_id', parentId);
        ga.addParam('sysparm_template_ids', selectedIds.join(','));

        ga.getXMLAnswer(function(answer) {
            _log("Server Response: " + answer);

            // --- STEP 3: Handle Parent UI Refresh ---
            // In Workspace, we use window.parent to access g_form
            var parentGForm = window.parent.g_form;

            if (answer.indexOf("error") > -1) {
                if (parentGForm) parentGForm.addErrorMessage("Failed to create tasks. Check system logs.");
                btn.disabled = false;
                btn.innerHTML = "Create Tasks";
            } else {
                // SUCCESS: Notify user on the ACTUAL record page
                if (parentGForm) {
                    parentGForm.clearMessages();
                    parentGForm.addInfoMessage("Successfully routed " + answer + " task(s).");
                    
                    // Force-refresh the related lists so the tasks appear immediately
                    parentGForm.refreshRelatedLists(); 
                }
                
                // --- STEP 4: Close Modal Immediately ---
                handleWorkspaceClose();
            }
        });
    };
})();

 

 

DEMO - 

 

 
If you find it helpful in any way, please mark it Helpful or hit a Like button. It will help others to find this helpful article. 

 

Version history
Last update:
6 hours ago
Updated by:
Contributors