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 scripts — onClick 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