Get a first look at what's coming. The Developer Passport Australia Release Preview kicks off March 12. Dive in! 

martinvirag
Mega Sage
Hi everyone!
 
I ran into a problem that I didn’t see covered anywhere else, so I wanted to share a practical solution that might help others too.

 

In short: When a user triggers an operation by using the classic UI Action, that takes more time, Workspace doesn't provide feedback by default. Instead of UI messages, here’s a cleaner and more visual way to show that something is happening in the background.
 

Long: In the classic ServiceNow UI, a UI Action button typically provides visual feedback by reloading the form, making the result of the action immediately visible. This behavior occurs when the logic runs on the server side, or when client-side code explicitly triggers a form submission using gsftSubmit().

In Workspace (e.g., SOW, CSM Workspace), the same button behaves more like client-side UI Action. The form is not automatically reloaded, and the gsftSubmit() function is not available in this context. As a result, even if server-side logic is executed in the background, the user does not receive built-in visual feedback, and there is no default indication that the action has been triggered or is in progress.

This becomes a real problem when a button click triggers an external integration or any other long-running process. Staying with the integration example: The round trip can take several seconds: the request leaves ServiceNow, the external system processes it, and a response is consumed. During that window the user has no way of knowing whether the operation is still running, has succeeded, or has silently failed. This often leads to users becoming frustrated, clicking the button multiple times, or assuming nothing happened.

The pattern described in this post solves that by opening a modal with a visual indicator, such as a spinner, the moment the button is clicked, running the server-side logic in the background, and replacing the spinner with a clear success or error result once the operation completes.

Components involved:

 

Component

 

Purpose

 

UI Action

Opens the modal, passes context via URL params

UI Page (HTML)

Renders the spinner and result views

UI Page (Client Script)

Calls the server via GlideAjax, updates the UI

Script Include (Ajax)

Executes the server-side logic and returns JSON

UI Messages

Translatable strings for all user-facing text

 

Script Include

Create a Script Include extending global.AbstractAjaxProcessor

 

var MyOperationAjax = Class.create();
MyOperationAjax.prototype = Object.extendsObject(global.AbstractAjaxProcessor, {

    execute: function() {
        const sysId = this.getParameter('sysparm_sys_id');
        const table = this.getParameter('sysparm_table') || 'your_default_table';

        const gr = new GlideRecord(table);
        if (!gr.get(sysId)) {
            return this.setAnswer(JSON.stringify({ status: 'error', message: 'Record not found.' }));
        }

        // Run your business logic here
        const result = new YourBusinessLogicClass().execute(gr);

        if (result.success) {
            this.setAnswer(JSON.stringify({ status: 'success', message: gs.getMessage('your_success_msg') }));
        } else {
            this.setAnswer(JSON.stringify({ status: 'error', message: result.errormsg || gs.getMessage('your_error_msg') }));
        }
    },

    type: 'MyOperationAjax'
});

 

Key rules to consider:

  • Always return setAnswer(JSON.stringify(...)) with a consistent { status, message } shape.

  • Use gs.getMessage() for all user-facing strings.

  • Accept sysparm_table if your logic can run on multiple tables, default to the primary one for backwards compatibility.

UI Page

 

HTML:

Create a UI Page (e.g. my_operation_spinner). In the HTML add:

<?xml version="1.0" encoding="utf-8" ?>
<j:jelly trim="false" xmlns:j="jelly:core" xmlns:g="glide" xmlns:j2="null" xmlns:g2="null">
    <div style="display:flex;flex-direction:column;align-items:center;justify-content:center;padding:32px;gap:16px;font-family:sans-serif;min-height:120px;">

        <!-- Spinner view: shown while the Ajax call is in flight -->
        <div id="spinner_view" style="display:flex;flex-direction:column;align-items:center;gap:12px;">
            <div style="width:36px;height:36px;border:4px solid #e0e0e0;border-top-color:#1d4ed8;border-radius:50%;animation:spin 0.8s linear infinite;"></div>
            <div style="font-size:14px;color:#555;">${gs.getMessage('your_processing_msg')}</div>
        </div>

        <!-- Result view: hidden until Ajax returns -->
        <div id="result_view" style="display:none;flex-direction:column;align-items:center;gap:12px;">
            <div id="result_icon" style="font-size:40px;line-height:1;"></div>
            <div id="result_message" style="font-size:13px;text-align:center;max-width:280px;"></div>
            <button onclick="closeSpinnerModal();" style="margin-top:8px;padding:6px 20px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:13px;">
                ${gs.getMessage('your_close_msg')}
            </button>
        </div>

    </div>
    <style>
        @keyframes spin { to { transform: rotate(360deg); } }
    </style>
</j:jelly>

 

Key rules to consider:

  • Use ${gs.getMessage('key')} for all visible text, if translation is needed

  • Keep two distinct views: one for the in-progress state, one for the result. Toggle between them in the client script.

  • The close button triggers closeSpinnerModal(), defined in the client script.

 

Client script


In the UI Page (e.g. my_operation_spinner). In the Client Script add:

 

function closeSpinnerModal() {
    window.location.href = window.location.href + '&done=1';
}


(function() {
    const params = new URLSearchParams(window.location.search);

    const sysId = params.get('sysparm_record_id') || params.get('sysparm_sys_id');
    const table = params.get('sysparm_table') || 'your_default_table';

    const ga = new GlideAjax('x_yourscope.MyOperationAjax');
    ga.addParam('sysparm_name', 'execute');
    ga.addParam('sysparm_sys_id', sysId);
    ga.addParam('sysparm_table', table);
    ga.getXMLAnswer(function(response) {
        const parsed = JSON.parse(response);

        document.getElementById('spinner_view').style.display = 'none';

        const iconEl = document.getElementById('result_icon');
        const msgEl  = document.getElementById('result_message');

        if (parsed.status === 'success') {
            iconEl.innerHTML = '&#10003;'; // ✓
            iconEl.style.color = '#16a34a';
        } else {
            iconEl.innerHTML = '&#10007;'; // ✗
            iconEl.style.color = '#dc2626';
        }
        msgEl.textContent = parsed.message;

        document.getElementById('result_view').style.display = 'flex';
    });
})();

 

Key rules to consider:

  • Pass all context the server needs via URL params; read with URLSearchParams (if ES6 or above is enabled)

  • Always hide the spinner before showing the result, even on error.


UI Action

On the form where you want the button to appear add this code to the Workspace code:

 

function onClick(g_form) {
    g_modal.showFrame({
        url: '/your_ui_page_name.do?sysparm_record_id=' + g_form.getUniqueValue()
           + '&sysparm_table=' + g_form.getTableName(),
        title: getMessage('your_modal_title_msg'),
        size: 'sm',
        height: 200,
        showClose: false,
        closeOnEscape: false,
        autoCloseOn: 'URL_CHANGED'
    });
}

 

Key rules to consider:

  • Always pass sysparm_table via g_form.getTableName() so the Script Include doesn't need to hardcode a table — this makes the pattern reusable across multiple tables.

  • Set showClose: false and closeOnEscape: false to prevent users from dismissing it before the operation completes.

  • autoCloseOn property is the key here. The modal panel in Workspace can be opened using the g_modal.showFrame({...}) function. This property has a special attribute (URL_CHANGED), which can be used to trigger the auto-close action instead of a manual action.

 

UI Messages

 

Register every user-facing string in System UI → Messages:

 

Key

English default

your_processing_msg

Processing…

your_close_msg

Close

your_success_msg

Operation completed successfully.

your_error_msg

Operation failed. Please try again.

your_modal_title_msg

Processing

 

Translations for other languages are added as additional rows with the same key

 

End results:

 

Screenshot 2026-03-17 at 14.24.02.png

Screenshot 2026-03-17 at 14.24.08.png

Screenshot 2026-03-17 at 14.24.26.png

Note : AI was used in this article mainly to generalize artifact names, add helpful guide comments to the code, and improve grammar and clarity.
Version history
Last update:
yesterday
Updated by: