Skip to main content
Version: 4.9.0

UI Actions

Guide for creating ServiceNow UI Actions using the Fluent API. UI Actions are buttons, links, or context menu items that execute server-side or client-side logic on forms and lists.

When to Use

  • Adding buttons to a form (e.g., "Approve", "Mark Resolved", "Save & Close")
  • Adding buttons or links to a list view (banner buttons, row-level actions)
  • Adding context menu items (right-click options) on forms or lists
  • Executing server-side scripts on button click (update records, trigger workflows)
  • Executing client-side scripts (confirmations, field validation, redirects)
  • Creating workspace-compatible actions with V2 button rendering

Instructions

  1. Decide placement first: Choose whether the action appears on forms, lists, or both using the form and list nested objects.
  2. Choose client vs server: Set client.isClient: true for client-side scripts (confirmations, redirects). Leave unset or false for server-side scripts (record updates, GlideRecord queries). Server module scripts (import/export) only work with server-side actions.
    • For client-side actions: Use client.onClick to call the function, and define the function in script. Example: client.onClick: 'myFunction()' and script: 'function myFunction() { ... }'
  3. Set visibility: Use showInsert and showUpdate to control when the action appears (new records vs existing records). Use condition for dynamic visibility based on record state.
  4. Restrict by role: Use the roles array to limit which users see the action.
  5. Style appropriately: Use 'primary' for main actions, 'destructive' for dangerous operations (delete, cancel), 'unstyled' for secondary actions.
  6. Workspace compatibility: For workspace-enabled actions, set workspace.isConfigurableWorkspace: true and if creating a client-side script, additionally provide workspace.clientScriptV2

API Reference

For the complete property reference including all form, list, client, and workspace nested objects with their types and descriptions, see the uiaction-api topic.

Key Concepts

Form vs List Placement

PropertyTypeDescription
form.showButtonbooleanAdds a button to the form header
form.showLinkbooleanAdds a link below the form
form.showContextMenubooleanAdds to the right-click menu on form
form.stylestring'primary', 'destructive', or 'unstyled'
list.showButtonbooleanAdds a button at the bottom of the list
list.showBannerButtonbooleanAdds a button in the list banner
list.showLinkbooleanAdds a link in the list
list.showContextMenubooleanAdds to the right-click menu on list rows
list.showListChoicebooleanAdds to dropdown menus of choice fields
list.showSaveWithFormButtonbooleanShows the button when saving the form from a related list
list.stylestring'primary', 'destructive', or 'unstyled'

Client-Side vs Server-Side

AspectServer-Side (default)Client-Side (client.isClient: true)
Script runsOn the serverIn the browser
current accessFull GlideRecordNot available (use g_form)
Module supportYes (import/export)No — string scripts only
Script propertyscript (string or module function)script (function definition) + client.onClick (function call)
Use forRecord updates, GlideRecord queriesConfirmations, redirects, form validation

Client-side pattern: Define the function in script, call it in client.onClick:

client: { isClient: true, onClick: 'myFunction()' },
script: `function myFunction() { /* logic here */ }`

Function naming: Use unique, descriptive function names for each UI Action on a table. In UI16, multiple actions with the same function name on the same form can conflict (e.g., two actions both defining function validate()). While Workspace/Lit UI handles this correctly with closures, UI16 may execute the wrong function. Use specific names like validateIncidentForm() or approveRequestAction() instead of generic names.

Visibility Controls

PropertyDescription
showInsertShow on new records (insert mode)
showUpdateShow on existing records (update mode)
showQueryShow when a list filter is applied
showMultipleUpdateShow when multiple records are selected (bulk edit)
conditionScript/condition controlling dynamic visibility (e.g., "current.state == 'new'")
rolesArray of role names restricting who sees the action
includeInViewsOnly show in these specific views
excludeFromViewsHide from these specific views

Additional Properties

PropertyDescription
isolateScriptRun the script in an isolated scope (prevents global variable conflicts)
messagesArray of user-facing messages accessible via getMessage() or getMessages() in client scripts
commentsInternal developer notes (not visible to end users)
overridesReference to another UI Action for domain separation - used when a UI Action on a subdomain overrides a UI Action on a parent domain
orderNumeric value controlling button position (lower numbers appear first, default is 100)
hintTooltip text shown when user hovers over the button

Workspace Compatibility

For actions to work in ServiceNow Workspace, set workspace properties:

workspace: {
isConfigurableWorkspace: true,
showFormButtonV2: true,
showFormMenuButtonV2: true,
clientScriptV2: `function onClick(g_form) {
// V2 client script
}`,
}

Table Inheritance and Overrides

UI Actions use table inheritance and deduplication to determine what gets shown to users:

Inheritance:

  • UI Actions defined on a parent table (e.g., task) automatically appear on child tables (e.g., incident, problem)
  • Child tables inherit all parent UI Actions unless explicitly overridden

Deduplication:

  • By action name: If a child table defines a UI Action with the same actionName as a parent action, the child action overrides the parent
  • By name AND action name: If two actions have the same name and actionName, ServiceNow displays only the one with the lower order value

Override pattern (table inheritance):

  1. Identify the parent action's actionName (query sys_ui_action table filtered by table and name if needed)
  2. Create a child action with the same actionName as the parent — this automatically overrides the parent action
  3. Set a lower order value if you want your action to take precedence over siblings with matching name and actionName

Examples

Combined Form and List Button with Client-Side Confirmation

UI Action with both form and list placement, including client-side confirmation before server-side processing:

import { UiAction } from '@servicenow/sdk/core'

UiAction({
$id: Now.ID['confirm-delete'],
table: 'x_myapp_request', // custom table
name: 'Delete',
actionName: 'confirm_delete',
showUpdate: true,
showMultipleUpdate: true,
form: {
showButton: true,
style: 'primary',
},
list: {
showBannerButton: true,
style: 'primary',
},
condition: "current.state == '4'",
client: {
isClient: true,
onClick: 'confirmAndDelete()',
},
script: `
var viewType;
var selectedSysId;

function confirmAndDelete() {
selectedSysId = typeof g_list != 'undefined' ? g_list.getChecked() : g_form.getUniqueValue();
viewType = typeof g_list != 'undefined' ? 'list' : 'form';
if(!selectedSysId || selectedSysId.length ==0)
return;
deleteListEntryRecords();
}

function deleteListEntryRecords() {
var dialogClass = typeof GlideModal != 'undefined' ? GlideModal : GlideDialogWindow;
var dlg = new dialogClass('delete_document_list_entry');
dlg.setTitle(new GwtMessage().getMessage('Confirmation'));
dlg.setWidth(300);
dlg.setPreference('sysparm_list_view', viewType);
if (viewType == 'list') {
dlg.setPreference('sysparm_document_list_entry_sys_id_list', selectedSysId);
} else
dlg.setPreference('sysparm_document_list_entry_sys_id', selectedSysId);
dlg.render();
}
`,
})

Form Button with Server-Side Script

import { UiAction } from '@servicenow/sdk/core'

UiAction({
$id: Now.ID['mark-resolved'],
table: 'x_myapp_request',
name: 'Mark as Resolved',
actionName: 'mark_resolved',
showUpdate: true,
form: {
showButton: true,
style: 'primary',
},
condition: "current.state != 'resolved'",
script: `current.state = 'resolved';
current.resolution_code = 'Solved (Permanently)';
current.update();
action.setRedirectURL(current);`,
})

List Banner Button with Role Restriction

import { UiAction } from '@servicenow/sdk/core'

UiAction({
$id: Now.ID['bulk-assign'],
table: 'x_myapp_task',
name: 'Assign to Me',
actionName: 'bulk_assign',
showUpdate: true,
showMultipleUpdate: true,
list: {
showBannerButton: true,
showButton: true,
style: 'primary',
},
roles: ['itil'],
script: `current.assigned_to = gs.getUserID();
current.update();`,
})

Client-Side Confirmation with Workspace Support

import { UiAction } from '@servicenow/sdk/core'

UiAction({
$id: Now.ID['confirm-delete'],
table: 'x_myapp_request',
name: 'Delete Request',
actionName: 'confirm_delete',
showUpdate: true,
form: {
showButton: true,
style: 'destructive',
},
client: {
isClient: true,
onClick: 'confirmDelete()', // call the function defined in script
},
workspace: {
isConfigurableWorkspace: true,
showFormButtonV2: true,
clientScriptV2: `function onClick(g_form) {
if (confirm('Are you sure you want to delete this request?')) {
g_form.submit();
}
}`,
},
script: `function confirmDelete() {
if (confirm('Are you sure you want to delete this request?')) {
gsftSubmit(null, g_form.getFormElement(), 'confirm_delete');
}
}`,
})

Server-Side Module Script

import { UiAction } from '@servicenow/sdk/core'
import { escalateRequest } from '../../server/actions/escalate-request'

UiAction({
$id: Now.ID['escalate'],
table: 'x_myapp_request',
name: 'Escalate',
actionName: 'escalate_request',
showUpdate: true,
form: {
showButton: true,
style: 'destructive',
},
roles: ['itil', 'admin'],
order: 200,
script: escalateRequest,
})

Client-Side with Messages and onClick

import { UiAction } from '@servicenow/sdk/core'

UiAction({
$id: Now.ID['notify-user'],
table: 'x_myapp_request',
name: 'Notify User',
actionName: 'notify_user',
showUpdate: true,
form: {
showButton: true,
style: 'primary',
},
client: {
isClient: true,
onClick: 'notifyUser()', // call the function defined in script
},
messages: ['User has been notified successfully'],
hint: 'Send notification to the assigned user',
isolateScript: true,
order: 150,
script: `function notifyUser() {
var msg = getMessage('User has been notified successfully');
alert(msg);
g_form.addInfoMessage(msg);
}`
})

Context Menu with Server-Side Script

import { UiAction } from '@servicenow/sdk/core'

UiAction({
$id: Now.ID['close-task'],
table: 'task',
name: 'Close Task',
actionName: 'close_task',
showUpdate: true,
form: {
showContextMenu: true,
},
condition: "current.state != 3",
roles: ['itil'],
script: `current.state = 3;
current.close_notes = 'Closed via context menu action';
current.closed_at = new GlideDateTime();
current.closed_by = gs.getUserID();
current.update();
gs.addInfoMessage('Task closed successfully');
action.setRedirectURL(current);`,
})

Client-Side Redirect Action

import { UiAction } from '@servicenow/sdk/core'

UiAction({
$id: Now.ID['view-related-items'],
table: 'x_myapp_request',
name: 'View Related Items',
actionName: 'view_related_items',
showUpdate: true,
form: {
showLink: true,
},
client: {
isClient: true,
onClick: 'viewRelatedItems()',
},
script: `function viewRelatedItems() {
var requestId = g_form.getUniqueValue();
var url = 'x_myapp_related_items_list.do?sysparm_query=request=' + requestId;
window.location.href = url;
}`,
})

List Banner Button with Client-Side Confirmation

import { UiAction } from '@servicenow/sdk/core'

UiAction({
$id: Now.ID['bulk-delete-items'],
table: 'x_myapp_inventory',
name: 'Delete Selected Items',
actionName: 'bulk_delete_items',
showUpdate: true,
showMultipleUpdate: true,
list: {
showBannerButton: true,
style: 'destructive',
},
roles: ['admin'],
client: {
isClient: true,
onClick: 'confirmBulkDelete()',
},
script: `function confirmBulkDelete() {
var count = g_list.getChecked().length;
if (count === 0) {
alert('Please select at least one item to delete');
return;
}
if (confirm('Are you sure you want to delete ' + count + ' item(s)? This action cannot be undone.')) {
gsftSubmit(null, g_form.getFormElement(), 'bulk_delete_items');
}
}`,
})

Server-Side GlideRecord Query with Multiple Updates

import { UiAction } from '@servicenow/sdk/core'

UiAction({
$id: Now.ID['assign-related-tasks'],
table: 'x_myapp_project',
name: 'Assign All Tasks to Me',
actionName: 'assign_related_tasks',
showUpdate: true,
form: {
showButton: true,
style: 'primary',
},
condition: "current.state == 'in_progress'",
script: `var taskGr = new GlideRecord('x_myapp_task');
taskGr.addQuery('project', current.sys_id);
taskGr.addQuery('assigned_to', '');
taskGr.query();

var assignedCount = 0;
while (taskGr.next()) {
taskGr.assigned_to = gs.getUserID();
taskGr.assigned_date = new GlideDateTime();
taskGr.update();
assignedCount++;
}

gs.addInfoMessage('Assigned ' + assignedCount + ' tasks to you');
action.setRedirectURL(current);`,
})

Client-Side Field Validation with Messages

import { UiAction } from '@servicenow/sdk/core'

UiAction({
$id: Now.ID['validate-and-submit'],
table: 'x_myapp_form',
name: 'Validate & Submit',
actionName: 'validate_and_submit',
showUpdate: true,
form: {
showButton: true,
style: 'primary',
},
client: {
isClient: true,
onClick: 'validateAndSubmit()',
},
messages: ['Validation failed - please fix the following errors', 'Form validated successfully'],
hint: 'Validate all required fields before submitting',
order: 100,
script: `function validateAndSubmit() {
var errors = [];

if (!g_form.getValue('name')) {
errors.push('Name is required');
}
if (!g_form.getValue('email')) {
errors.push('Email is required');
}
if (!g_form.getValue('description')) {
errors.push('Description is required');
}

if (errors.length > 0) {
var errorMsg = getMessage('Validation failed - please fix the following errors') + ':\\n' + errors.join('\\n');
alert(errorMsg);
return;
}

g_form.addInfoMessage(getMessage('Form validated successfully'));
g_form.submit();
}`,
})

Combined Client and Server Script - Confirmation with Server Processing

import { UiAction } from '@servicenow/sdk/core'

UiAction({
$id: Now.ID['approve-with-confirmation'],
table: 'x_myapp_approval_request',
name: 'Approve Request',
actionName: 'approve_request',
showUpdate: true,
form: {
showButton: true,
style: 'primary',
},
condition: "current.state == 'pending'",
client: {
isClient: true,
onClick: 'approveWithConfirmation()',
},
script: `
// Client-side: Confirmation dialog
function approveWithConfirmation() {
var comments = prompt('Please enter approval comments (optional):');
if (comments !== null) {
// User clicked OK (even if empty) - proceed with server-side submission
g_form.setValue('approval_comments', comments);
gsftSubmit(null, g_form.getFormElement(), 'approve_request');
}
// If user clicked Cancel, do nothing
}

// Guard ensures server-side code only runs on the server, not in the browser
// Required for UI Actions that contain both client-side and server-side logic in the same script
if (typeof window == 'undefined') {
processApproval();
}

function processApproval() {

// Server-side: Process approval
current.state = 'approved';
current.approved_by = gs.getUserID();
current.approved_date = new GlideDateTime();
current.update();

// Send notification
var approver = gs.getUser();
gs.eventQueue('approval.granted', current, approver.getDisplayName(), current.approval_comments);

gs.addInfoMessage('Request approved successfully');
action.setRedirectURL(current);
}
`,
})

Combined Client and Server Script - Validation with Complex Server Logic

import { UiAction } from '@servicenow/sdk/core'

UiAction({
$id: Now.ID['submit-for-review'],
table: 'x_myapp_document',
name: 'Submit for Review',
actionName: 'submit_for_review',
showUpdate: true,
form: {
showButton: true,
style: 'primary',
},
condition: "current.state == 'draft'",
client: {
isClient: true,
onClick: 'validateAndSubmit()',
},
script: `
// Client-side: Validate required fields before submission
function validateAndSubmit() {
var errors = [];

if (!g_form.getValue('title')) {
errors.push('Title is required');
}
if (!g_form.getValue('category')) {
errors.push('Category must be selected');
}
if (!g_form.getValue('assigned_reviewer')) {
errors.push('Reviewer must be assigned');
}

if (errors.length > 0) {
alert('Please fix the following errors:\\n\\n' + errors.join('\\n'));
return;
}

if (confirm('Submit this document for review? You will not be able to edit it until review is complete.')) {
gsftSubmit(null, g_form.getFormElement(), 'submit_for_review');
}
}

// Guard ensures server-side code only runs on the server, not in the browser
// Required for UI Actions that contain both client-side and server-side logic in the same script
if (typeof window == 'undefined') {
processReview();
}

function processReview() {
// Server-side: Create review task and update document state
current.state = 'in_review';
current.submitted_date = new GlideDateTime();
current.submitted_by = gs.getUserID();
current.update();

// Create review task
var reviewTask = new GlideRecord('x_myapp_review_task');
reviewTask.initialize();
reviewTask.document = current.sys_id;
reviewTask.assigned_to = current.assigned_reviewer;
reviewTask.due_date = new GlideDateTime();
reviewTask.due_date.addDaysLocalTime(3); // 3 day SLA
reviewTask.state = 'open';
reviewTask.insert();

action.setRedirectURL(current);
}
`,
})

Override existing Form Button in child table

import { UiAction } from '@servicenow/sdk/core'

UiAction({
$id: Now.ID['custom-resolve-form'],
table: 'custom_incident', // custom incident table
name: 'Resolve',
actionName: 'resolve_incident', // must match parent action name
showUpdate: true,
form: {
showButton: true,
style: 'primary',
},
condition: "current.state != 6",
client: {
isClient: true,
onClick: 'customResolveValidation()',
},
script: `
// Client-side: Validate required fields
function customResolveValidation() {
if (!g_form.getValue('close_notes') || !g_form.getValue('close_code')) {
alert('Close Notes and Close Code are required');
return;
}
if (confirm('Resolve this incident?')) {
gsftSubmit(null, g_form.getFormElement(), 'resolve');
}
}

// Guard ensures server-side code only runs on the server, not in the browser
// Required for UI Actions that contain both client-side and server-side logic in the same script
if (typeof window == 'undefined') {
// Server-side: Process resolution
current.state = 6;
current.resolved_at = new GlideDateTime();
current.resolved_by = gs.getUserID();
current.update();
gs.addInfoMessage('Incident resolved successfully');
action.setRedirectURL(current);
}
`,
})

Override List Context Menu

import { UiAction } from '@servicenow/sdk/core'

UiAction({
$id: Now.ID['custom-resolve-list'],
table: 'custom_incident', // custom incident table
name: 'Resolve',
actionName: 'resolve_incident', // must match parent action - automatically overrides parent due to same actionName
showUpdate: true,
list: {
showContextMenu: true, // right-click menu on list
},
condition: "current.state != 6",
script: `
// Validate before resolving
if (!current.close_notes || !current.close_code) {
gs.addErrorMessage('Close Notes and Close Code required');
action.setRedirectURL(current);
return;
}

current.state = 6;
current.resolved_at = new GlideDateTime();
current.resolved_by = gs.getUserID();
current.update();
gs.addInfoMessage('Incident resolved successfully');
action.setRedirectURL(current);
`,
})

Avoidance

  • Never use module scripts for client-side actions — modules only work on server-side. Client actions require string scripts. If the build reports a type mismatch, the action is client-side and must use string scripts or Now.include().
  • Never put inline code in client.onClick — define the function in script and call it from onClick. Example: client.onClick: 'myFunction()' with script: 'function myFunction() { ... }'
  • Never set both form and list properties without intentional design — decide whether the action applies to forms, lists, or both. Setting both without purpose creates confusing UX.
  • Never create UI Actions for view switching — always use View Rules (sysrule_view) for automatic view routing based on conditions or device type.
  • Never use showMultipleUpdate without testing — bulk actions run on every selected record and can be expensive. Ensure the script handles bulk operations efficiently.
  • Never hardcode record states in scripts — use constants or choice values that match the table's state model.
  • Never omit condition for destructive actions — always gate dangerous operations behind a condition (e.g., state check) and role restriction.
  • Never forget action.setRedirectURL(current) — server-side scripts that update the current record should redirect back to the form to show the updated state.
  • Never use client.onClick for server-side scriptsonClick is for client-side logic only. Use script property for server-side actions.
  • Never rely on isolateScript as a security measure — it prevents variable pollution, not malicious code execution. Use proper ACLs and role restrictions for security.
  • Never use workspace properties without testing in Configurable Workspace — workspace V2 rendering (showFormButtonV2, clientScriptV2) only applies in workspace environments; test both classic and workspace UIs.

See Also

  • For view switching and automatic view routing → use View Rules (sysrule_view). See the view-rule-guide
  • For client-side field validation and dynamic behavior → use Client Scripts or UI Policies
  • For securing UI Actions by role → see the roles property and ACL configuration
  • For programmatic action invocation → use the actionName property as a reference in scripts
  • For full API property reference and type signatures → see the uiaction-api topic