What It Is

UI Actions are ServiceNow's extensibility mechanism for adding custom interactive elements to forms and lists — buttons, context menu items, related links, and choice actions that execute your business logic when users click them. They solve the fundamental problem of bridging user intent with server-side processing, giving you programmatic control over what happens when someone needs to perform an action that doesn't exist in the base platform. Without UI Actions, you're stuck with ServiceNow's built-in buttons and whatever workflows you can cobble together with client scripts and business rules — which means no clean way to trigger complex server-side operations on demand.

Architecturally, UI Actions live at the presentation layer but can execute code in two completely different contexts: client-side JavaScript that runs in the user's browser with access to DOM manipulation and form fields, or server-side JavaScript that executes on the ServiceNow instance with full database access and GlideRecord capabilities. This dual-execution model is what makes them powerful and dangerous — you can write a UI Action that looks like it should work but fails because you're trying to use GlideRecord in client-side code or manipulate form fields from server-side code. The platform determines execution context based on the UI Action's configuration, not your code's content.

Under the hood, ServiceNow processes UI Actions through its rendering engine when building forms and lists, evaluating condition scripts to determine visibility, then either embedding client-side code directly into the page's JavaScript or setting up AJAX endpoints for server-side execution. When you create a server-side UI Action, ServiceNow automatically generates a unique URL endpoint that accepts POST requests containing the current record's sys_id and other context data. For client-side UI Actions, your JavaScript gets injected into the form's script context where it has access to the g_form API and can manipulate the user interface directly.

You absolutely cannot build sophisticated ServiceNow applications without UI Actions because they're the only clean way to execute custom logic on user demand while maintaining proper separation of concerns. Yes, you could hack together something with onChange client scripts or scheduled jobs, but you'd be fighting the platform instead of using its intended extension points. UI Actions are how you add 'Approve All Items' buttons to service catalog requests, 'Escalate to Manager' links on incident forms, and 'Bulk Update' options on list views — the kind of workflow triggers that turn a basic ITSM implementation into something that actually fits your organization's processes.

System administrators typically use UI Actions for simple form manipulations and data updates that don't require complex business logic — things like preset field values or basic record state changes. Developers build UI Actions for workflow integration, external system calls, and multi-step processes that need proper error handling and transaction management. Architects design UI Action patterns as part of larger application frameworks, often creating reusable templates and establishing standards for how different types of actions should behave across an enterprise implementation. The complexity scales from 'set priority to high' to 'orchestrate a complex approval workflow with external API calls and email notifications.'

UI Actions are closely related to Business Rules and Script Includes, but serve a fundamentally different purpose in the application architecture. While Business Rules respond automatically to database operations (insert, update, delete), UI Actions respond to explicit user interactions and can execute either before or after data changes. Script Includes provide reusable server-side functions that UI Actions often call, creating a clean separation between presentation logic and business logic. Unlike Workflow Activities, which operate asynchronously as part of a larger process, UI Actions provide immediate feedback to users and typically complete their execution within the context of a single HTTP request.

How It Works Under the Hood

When ServiceNow renders a form or list, it queries the sys_ui_action table to find all UI Actions that apply to the current table and context (form, list, related list). For each UI Action, the platform evaluates any condition scripts to determine visibility, then processes them differently based on their execution type. Client-side UI Actions get their JavaScript code embedded directly into the page's HTML within script tags, making their functions available in the browser's global scope. Server-side UI Actions get registered as endpoint handlers that the browser can call via AJAX, with ServiceNow automatically handling the request routing and parameter passing.

The critical architectural detail that trips up many developers is that client-side and server-side UI Actions operate in completely isolated execution contexts with different APIs, security models, and data access patterns. Client-side code runs in the user's browser with access to the g_form, g_list, and g_user objects but cannot directly query the database or call server-side Script Includes. Server-side code runs on the ServiceNow instance with full GlideRecord access and can call other server-side APIs, but cannot manipulate form fields or access browser-specific functionality. This separation is enforced at runtime — trying to use the wrong APIs results in 'undefined' errors that can be confusing if you don't understand the execution context.

For server-side UI Actions, ServiceNow implements a sophisticated request lifecycle that includes automatic transaction management, security context validation, and error handling. The platform creates a unique execution context for each request that includes the current user's roles, the target record's ACL permissions, and any relevant business rule processing. Most developers don't realize that server-side UI Actions automatically run within database transactions — if your code throws an exception, ServiceNow rolls back any database changes made during the UI Action's execution, which can save you from data consistency issues but can also mask problems if you're not handling errors properly.

The Server-Side Request Lifecycle

  1. User clicks the UI Action button or menu item, triggering a POST request to the UI Action's generated endpoint URL
  2. ServiceNow validates the request parameters, checks user permissions, and verifies the target record exists and is accessible
  3. Platform creates an execution context with a GlideRecord instance pointing to the target record (available as 'current' variable)
  4. Database transaction begins automatically — all subsequent database operations are part of this transaction
  5. UI Action's script code executes with full server-side API access, including GlideRecord, GlideSystem, and Script Include calls
  6. If script completes successfully, transaction commits and any database changes become permanent
  7. Platform generates HTTP response, typically redirecting to the updated record or returning to the previous page
  8. If any exception occurs during execution, transaction rolls back automatically and ServiceNow displays error message to user
Free Newsletter

Enjoying this? Get one deep-dive per week.

Join 1,000+ ServiceNow pros — scripts, GlideRecord patterns, Flow Designer techniques, and career moves. Free.

No spam · Unsubscribe anytime

The Core Pattern

UI Action — Client Side Validation.js
function validateAndSubmit() {
    // Always validate required fields before server round-trip
    if (!g_form.getValue('short_description')) {
        alert('Short description is required');
        return;
    }
    
    // Check for unsaved changes that might be lost
    if (g_form.isModified()) {
        if (!confirm('This action will save your changes. Continue?')) {
            return;
        }
    }
    
    // Call server-side UI Action via AJAX to preserve form state
    var ga = new GlideAjax('IncidentEscalationUtils');
    ga.addParam('sysparm_name', 'escalateToManager');
    ga.addParam('sysparm_sys_id', g_form.getUniqueValue());
    ga.addParam('sysparm_urgency', g_form.getValue('urgency'));
    
    // Handle async response and update form accordingly
    ga.getXML(function(response) {
        var answer = response.responseXML.documentElement.getAttribute('answer');
        if (answer === 'success') {
            g_form.addInfoMessage('Incident escalated to manager');
            g_form.reload(); // Refresh to show server-side changes
        } else {
            alert('Escalation failed: ' + answer);
        }
    });
}
Script Include — IncidentEscalationUtils.js
var IncidentEscalationUtils = Class.create();
IncidentEscalationUtils.prototype = Object.extendsObject(AbstractAjaxProcessor, {
    
    escalateToManager: function() {
        // Get parameters passed from client-side AJAX call
        var incidentId = this.getParameter('sysparm_sys_id');
        var currentUrgency = this.getParameter('sysparm_urgency');
        
        // Server-side validation with proper error handling
        var incident = new GlideRecord('incident');
        if (!incident.get(incidentId)) {
            return 'error: incident not found';
        }
        
        // Business logic that requires database access
        var userGr = new GlideRecord('sys_user');
        if (userGr.get(incident.caller_id) && userGr.manager) {
            incident.assigned_to = userGr.manager;
            incident.urgency = Math.max(parseInt(currentUrgency), 2); // Escalate urgency
            incident.work_notes = 'Escalated to manager via UI Action';
            incident.update();
            return 'success';
        }
        
        return 'error: no manager found for caller';
    },
    
    type: 'IncidentEscalationUtils'
});

Real-World Scenarios

Bulk Approval for Service Catalog Requests

Your procurement team needs to approve multiple related catalog requests at once instead of clicking through each approval individually. The UI Action appears on request forms when the current user is an approver and there are related pending requests from the same requester.

UI Action — Bulk Approve Related Requests.js
function bulkApproveRelated() {
    // Get current request details for finding related items
    var currentRequestId = g_form.getUniqueValue();
    var requesterId = g_form.getValue('requested_for');
    
    if (!requesterId) {
        alert('Cannot find requester for this request');
        return;
    }
    
    // Confirm action before processing multiple approvals
    if (!confirm('This will approve all pending requests for this user. Continue?')) {
        return;
    }
    
    // Call server-side processing via AJAX
    var ga = new GlideAjax('RequestApprovalUtils');
    ga.addParam('sysparm_name', 'bulkApproveByRequester');
    ga.addParam('sysparm_current_request', currentRequestId);
    ga.addParam('sysparm_requester_id', requesterId);
    
    ga.getXML(function(response) {
        var result = response.responseXML.documentElement.getAttribute('answer');
        var count = response.responseXML.documentElement.getAttribute('count');
        alert('Approved ' + count + ' requests');
        g_form.reload();
    });
}
Script Include — RequestApprovalUtils.js
var RequestApprovalUtils = Class.create();
RequestApprovalUtils.prototype = Object.extendsObject(AbstractAjaxProcessor, {
    
    bulkApproveByRequester: function() {
        var currentRequestId = this.getParameter('sysparm_current_request');
        var requesterId = this.getParameter('sysparm_requester_id');
        var approvedCount = 0;
        
        // Find all pending approvals for this requester that current user can approve
        var approvalGr = new GlideRecord('sysapproval_approver');
        approvalGr.addQuery('state', 'requested');
        approvalGr.addQuery('approver', gs.getUserID());
        approvalGr.query();
        
        while (approvalGr.next()) {
            // Check if this approval is for a request from our target requester
            var requestGr = new GlideRecord('sc_request');
            if (requestGr.get(approvalGr.sysapproval) && requestGr.requested_for == requesterId) {
                approvalGr.state = 'approved';
                approvalGr.approver_comments = 'Bulk approved via UI Action';
                approvalGr.update();
                approvedCount++;
            }
        }
        
        this.setAnswer(approvedCount.toString());
        this.setProperty('count', approvedCount);
    },
    
    type: 'RequestApprovalUtils'
});

Watch for approval workflow configuration — some organizations use advanced approval rules that might not trigger properly when you update sysapproval_approver records directly. Test thoroughly in sub-production environments and consider using the Workflow API for complex approval chains. Also validate that bulk operations don't violate any approval segregation of duties rules your organization might have implemented.

External System Integration with Error Recovery

Incident management process requires creating corresponding tickets in an external monitoring system when incidents reach Priority 1. The integration must handle network failures gracefully and provide clear feedback to users about the external system status.

UI Action — Create External Ticket.js
function createExternalTicket() {
    // Validate incident is ready for external escalation
    var priority = g_form.getValue('priority');
    var externalRef = g_form.getValue('u_external_ticket_id');
    
    if (priority != '1') {
        alert('External tickets can only be created for Priority 1 incidents');
        return;
    }
    
    if (externalRef) {
        alert('External ticket already exists: ' + externalRef);
        return;
    }
    
    // Show progress indicator for potentially slow external call
    g_form.addInfoMessage('Creating external ticket... Please wait.');
    
    var ga = new GlideAjax('ExternalTicketIntegration');
    ga.addParam('sysparm_name', 'createTicket');
    ga.addParam('sysparm_incident_id', g_form.getUniqueValue());
    ga.addParam('sysparm_summary', g_form.getValue('short_description'));
    
    ga.getXMLWait(); // Synchronous call to prevent user actions during integration
    var response = ga.getAnswer();
    
    if (response.startsWith('SUCCESS:')) {
        var ticketId = response.split(':')[1];
        g_form.setValue('u_external_ticket_id', ticketId);
        g_form.save();
    } else {
        alert('External ticket creation failed: ' + response);
    }
}
Script Include — ExternalTicketIntegration.js
var ExternalTicketIntegration = Class.create();
ExternalTicketIntegration.prototype = Object.extendsObject(AbstractAjaxProcessor, {
    
    createTicket: function() {
        var incidentId = this.getParameter('sysparm_incident_id');
        var summary = this.getParameter('sysparm_summary');
        
        try {
            // Build request payload with incident details
            var requestBody = {
                'title': summary,
                'description': 'Created from ServiceNow incident ' + incidentId,
                'severity': 'critical',
                'source': 'servicenow'
            };
            
            // External API call with proper timeout and retry logic
            var request = new sn_ws.RESTMessageV2('ExternalMonitoringAPI', 'POST');
            request.setRequestBody(JSON.stringify(requestBody));
            var response = request.execute();
            
            if (response.getStatusCode() == 201) {
                var responseBody = JSON.parse(response.getBody());
                return 'SUCCESS:' + responseBody.ticket_id;
            } else {
                gs.error('External API returned status: ' + response.getStatusCode());
                return 'ERROR: External system unavailable (HTTP ' + response.getStatusCode() + ')';
            }
            
        } catch (ex) {
            gs.error('External ticket creation failed: ' + ex.getMessage());
            return 'ERROR: Integration service temporarily unavailable';
        }
    },
    
    type: 'ExternalTicketIntegration'
});

External integrations in UI Actions are risky because they block the user interface during execution and can timeout if the external system is slow. Always implement proper timeout handling, use getXMLWait() judiciously, and consider queuing external calls as events for asynchronous processing. Test failure scenarios extensively — external systems will fail at the worst possible moments, and your UI Action needs to degrade gracefully without leaving data in an inconsistent state.

Dynamic Form Reconfiguration Based on Business Rules

Change management process requires different field sets and approval workflows based on the change's risk level and affected systems. Users need a 'Recalculate Risk Level' button that re-evaluates business rules and updates the form layout accordingly without losing their current work.

UI Action — Recalculate Change Risk.js
function recalculateRisk() {
    // Preserve current form data before server round-trip
    var formData = {
        'short_description': g_form.getValue('short_description'),
        'description': g_form.getValue('description'),
        'business_service': g_form.getValue('business_service'),
        'cmdb_ci': g_form.getValue('cmdb_ci'),
        'start_date': g_form.getValue('start_date')
    };
    
    // Call server-side risk calculation
    var ga = new GlideAjax('ChangeRiskCalculator');
    ga.addParam('sysparm_name', 'calculateRiskLevel');
    ga.addParam('sysparm_change_id', g_form.getUniqueValue());
    ga.addParam('sysparm_business_service', formData.business_service);
    ga.addParam('sysparm_cmdb_ci', formData.cmdb_ci);
    
    ga.getXML(function(response) {
        var newRiskLevel = response.responseXML.documentElement.getAttribute('risk_level');
        var requiredFields = response.responseXML.documentElement.getAttribute('required_fields');
        
        // Update form with calculated risk level
        g_form.setValue('risk', newRiskLevel);
        
        // Show/hide fields based on new risk level
        var fieldsArray = requiredFields.split(',');
        for (var i = 0; i < fieldsArray.length; i++) {
            g_form.setMandatory(fieldsArray[i], true);
            g_form.setVisible(fieldsArray[i], true);
        }
        
        g_form.addInfoMessage('Risk level updated to: ' + newRiskLevel);
    });
}
Script Include — ChangeRiskCalculator.js
var ChangeRiskCalculator = Class.create();
ChangeRiskCalculator.prototype = Object.extendsObject(AbstractAjaxProcessor, {
    
    calculateRiskLevel: function() {
        var changeId = this.getParameter('sysparm_change_id');
        var businessServiceId = this.getParameter('sysparm_business_service');
        var cmdbCiId = this.getParameter('sysparm_cmdb_ci');
        
        var riskLevel = 'low';
        var requiredFields = ['justification', 'test_plan'];
        
        // Business service criticality affects risk
        if (businessServiceId) {
            var bsGr = new GlideRecord('cmdb_ci_service');
            if (bsGr.get(businessServiceId) && bsGr.operational_status == '1') {
                riskLevel = 'medium';
                requiredFields.push('backout_plan', 'implementation_plan');
            }
        }
        
        // Production CI involvement escalates to high risk
        if (cmdbCiId) {
            var ciGr = new GlideRecord('cmdb_ci');
            if (ciGr.get(cmdbCiId) && ciGr.environment == 'production') {
                riskLevel = 'high';
                requiredFields.push('cab_required', 'vendor_contact');
            }
        }
        
        // Update the actual change record
        var changeGr = new GlideRecord('change_request');
        if (changeGr.get(changeId)) {
            changeGr.risk = riskLevel;
            changeGr.update();
        }
        
        this.setProperty('risk_level', riskLevel);
        this.setProperty('required_fields', requiredFields.join(','));
    },
    
    type: 'ChangeRiskCalculator'
});
⚠️

Client-side form manipulation after server calls can create race conditions if users modify fields while AJAX requests are in flight. Always preserve form state before server round-trips and be careful about overwriting user input. Consider using g_form.isModified() to detect conflicts and ask users how to resolve them.

The Classic Mistake

⚠️

Using client-side UI Actions to directly update records without proper validation or error handling.

Anti-pattern — Do Not Use This.js
// Client-side UI Action - WRONG approach
function updatePriority() {
    var gr = new GlideRecord('incident');
    gr.get(g_form.getUniqueValue());
    gr.setValue('priority', '1');
    gr.update();
    
    // Try to refresh the form
    g_form.save();
    alert('Priority updated to Critical!');
    
    // Hide fields based on new priority
    g_form.setVisible('work_notes', false);
    g_form.setMandatory('short_description', true);
}

This fails catastrophically because GlideRecord doesn't exist on the client side — you'll see "GlideRecord is not defined" in the browser console. Even if it worked, the client has no database access, so ServiceNow would reject the operation. The g_form.save() call happens immediately, overriding any server-side changes, and the form refresh creates a race condition where your field visibility changes get lost. Server-side Business Rules never fire because the update attempt fails before reaching the database.

The Fix.js
// Client-side UI Action - CORRECT approach
function updatePriority() {
    // Use GlideAjax to call server-side script
    var ga = new GlideAjax('IncidentUtils');
    ga.addParam('sysparm_name', 'updatePriority');
    ga.addParam('sysparm_incident_id', g_form.getUniqueValue());
    ga.addParam('sysparm_new_priority', '1');
    
    ga.getXML(function(response) {
        var answer = response.responseXML.documentElement.getAttribute('answer');
        if (answer === 'success') {
            g_form.setValue('priority', '1');
            g_form.showFieldMsg('priority', 'Priority updated to Critical', 'info');
        } else {
            g_form.showErrorBox('Failed to update priority: ' + answer);
        }
    });
}
💡

If your client-side UI Action needs to modify data, use GlideAjax to call server-side Script Includes. Client code handles UI, server code handles data.

Performance Rules

  1. Never use GlideRecord.query() without setLimit() in server-side UI Actions. Queries over 10,000 records trigger transaction timeouts after 30 seconds, causing the UI Action to fail silently and leaving users with spinning buttons.
  2. Avoid GlideAjax calls in client-side UI Actions that fire on form load or field changes. Each call adds 200-500ms network latency; more than 3 concurrent calls create browser congestion and make forms feel sluggish, generating user complaints about "slow ServiceNow."
  3. Don't use action.setRedirectURL() to complex list pages with extensive filtering. URLs over 2000 characters get truncated by browsers, breaking the redirect and sending users to blank pages or 404 errors.
  4. Never call gs.sleep() or synchronous web service calls in UI Actions. Any delay over 10 seconds triggers ServiceNow's transaction timeout, causing the action to abort and potentially corrupting partial database updates.
  5. Limit g_form.getValue() calls to under 20 per UI Action execution. Each call traverses the DOM; excessive usage slows form interactions and causes IE11 browsers to freeze temporarily.
  6. Avoid creating UI Actions with Condition scripts that query other tables. ServiceNow evaluates conditions on every page load; complex conditions add 1-3 seconds to form rendering and overwhelm the database connection pool during peak usage.
  7. Don't use GlideSystem.executeNow() for bulk operations in UI Actions. Operations affecting more than 100 records should use Scheduled Jobs instead; synchronous bulk updates block the user interface and can trigger system administrator alerts for excessive resource consumption.
  8. Restrict gs.eventQueue() usage to critical operations only. Each queued event consumes memory in the event queue; UI Actions that generate more than 50 events per execution can exhaust queue capacity and delay essential system notifications.

Side Effects & Platform Behavior

  • Server-side UI Actions trigger all standard Business Rules (before/after insert/update/delete) on modified records, but async Business Rules may not complete before the UI Action finishes, creating race conditions
  • Client-side UI Actions run in the user's browser session context, so they have access to g_user session variables but cannot access server-side user preferences or group memberships directly
  • ACLs evaluate normally for server-side UI Actions, but the gs.hasRole() context uses the UI Action's execution user, not necessarily the logged-in user in impersonation scenarios
  • UI Actions write execution details to syslog table when they fail, but successful executions only log to syslog_transaction if debug logging is enabled
  • Form submission UI Actions prevent normal form onSubmit Client Scripts from running if the UI Action script returns false, breaking form validation chains
  • List UI Actions operating on multiple records bypass individual record ACLs but still respect table-level ACLs, potentially allowing unauthorized bulk operations
  • UI Actions that modify sys_audit audited fields create audit entries with the UI Action name in the reason field, helping administrators track programmatic changes
  • Email notifications triggered by UI Action database changes use the UI Action's execution context for variable substitution, potentially showing system values instead of user-friendly display values
  • UI Actions break if their target table is extended after creation — the Table field must be manually updated to work on child tables
  • Related List UI Actions inherit the parent record's context variables, but current always refers to the related record, not the parent — a common source of script errors

Debugging When It Breaks

When UI Actions fail, users typically see buttons that don't respond, infinite loading spinners, or JavaScript errors in popup dialogs. The most common failure pattern is clicking a button that briefly shows "Loading..." then returns to its normal state without any visible effect. For client-side UI Actions, check the browser's Developer Tools Console (F12) for JavaScript errors like "Uncaught ReferenceError" or "Cannot read property of undefined."

Server-side UI Action failures require checking ServiceNow's System Logs under System Diagnostics > Logs > System Log > All. Look for entries with the source "UI Action" and error messages containing "Script error in UI Action" or "Transaction cancelled." Database timeout errors appear as "ORA-01013" (Oracle) or "Query was aborted" (MySQL). GlideAjax failures show up as HTTP 500 errors in the browser's Network tab and corresponding server errors mentioning the Script Include name.

Quick diagnostic checklist for non-working UI Actions:

  • Verify the UI Action's Active checkbox is checked and Table field matches your current form
  • Test the Condition script in a Background Script to ensure it returns true
  • Check user roles against any role requirements in the Show roles field
  • Add gs.info() statements at the beginning and end of server-side scripts to confirm execution
  • For client-side issues, add console.log() statements and monitor the browser console
  • Validate that current variable contains expected values by logging key fields

Quick Reference

  • Client-side UI Actions can't use GlideRecord, gs, or current — use GlideAjax instead
  • Form button UI Actions run before form submission; List UI Actions run against selected records or all visible records if none selected
  • Use action.setRedirectURL() for server-side navigation, window.open() for client-side
  • Context menu UI Actions appear on right-click; Related Link UI Actions show in the form's Related Links section
  • Order determines button sequence; lower numbers appear first (left-to-right on forms, top-to-bottom in lists)
  • Set Client field to true for browser execution, false for server execution
  • UI Actions inherit security context from the session user, not the record owner
  • Form submission UI Actions can prevent save by returning false; non-submission UI Actions can't block form operations
  • Condition scripts run on every page load; keep them simple or use UI Policies for complex field-based visibility
  • Mobile UI Actions require separate configuration and don't support complex JavaScript — use simple server-side operations only