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
- Decide placement first: Choose whether the action appears on forms, lists, or both using the
formandlistnested objects. - Choose client vs server: Set
client.isClient: truefor client-side scripts (confirmations, redirects). Leave unset orfalsefor server-side scripts (record updates, GlideRecord queries). Server module scripts (import/export) only work with server-side actions.- For client-side actions: Use
client.onClickto call the function, and define the function inscript. Example:client.onClick: 'myFunction()'andscript: 'function myFunction() { ... }'
- For client-side actions: Use
- Set visibility: Use
showInsertandshowUpdateto control when the action appears (new records vs existing records). Useconditionfor dynamic visibility based on record state. - Restrict by role: Use the
rolesarray to limit which users see the action. - Style appropriately: Use
'primary'for main actions,'destructive'for dangerous operations (delete, cancel),'unstyled'for secondary actions. - Workspace compatibility: For workspace-enabled actions, set
workspace.isConfigurableWorkspace: trueand if creating a client-side script, additionally provideworkspace.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
| Property | Type | Description |
|---|---|---|
form.showButton | boolean | Adds a button to the form header |
form.showLink | boolean | Adds a link below the form |
form.showContextMenu | boolean | Adds to the right-click menu on form |
form.style | string | 'primary', 'destructive', or 'unstyled' |
list.showButton | boolean | Adds a button at the bottom of the list |
list.showBannerButton | boolean | Adds a button in the list banner |
list.showLink | boolean | Adds a link in the list |
list.showContextMenu | boolean | Adds to the right-click menu on list rows |
list.showListChoice | boolean | Adds to dropdown menus of choice fields |
list.showSaveWithFormButton | boolean | Shows the button when saving the form from a related list |
list.style | string | 'primary', 'destructive', or 'unstyled' |
Client-Side vs Server-Side
| Aspect | Server-Side (default) | Client-Side (client.isClient: true) |
|---|---|---|
| Script runs | On the server | In the browser |
current access | Full GlideRecord | Not available (use g_form) |
| Module support | Yes (import/export) | No — string scripts only |
| Script property | script (string or module function) | script (function definition) + client.onClick (function call) |
| Use for | Record updates, GlideRecord queries | Confirmations, 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
| Property | Description |
|---|---|
showInsert | Show on new records (insert mode) |
showUpdate | Show on existing records (update mode) |
showQuery | Show when a list filter is applied |
showMultipleUpdate | Show when multiple records are selected (bulk edit) |
condition | Script/condition controlling dynamic visibility (e.g., "current.state == 'new'") |
roles | Array of role names restricting who sees the action |
includeInViews | Only show in these specific views |
excludeFromViews | Hide from these specific views |
Additional Properties
| Property | Description |
|---|---|
isolateScript | Run the script in an isolated scope (prevents global variable conflicts) |
messages | Array of user-facing messages accessible via getMessage() or getMessages() in client scripts |
comments | Internal developer notes (not visible to end users) |
overrides | Reference to another UI Action for domain separation - used when a UI Action on a subdomain overrides a UI Action on a parent domain |
order | Numeric value controlling button position (lower numbers appear first, default is 100) |
hint | Tooltip 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
actionNameas a parent action, the child action overrides the parent - By name AND action name: If two actions have the same
nameandactionName, ServiceNow displays only the one with the lowerordervalue
Override pattern (table inheritance):
- Identify the parent action's
actionName(querysys_ui_actiontable filtered bytableandnameif needed) - Create a child action with the same
actionNameas the parent — this automatically overrides the parent action - Set a lower
ordervalue if you want your action to take precedence over siblings with matchingnameandactionName
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 inscriptand call it fromonClick. Example:client.onClick: 'myFunction()'withscript: 'function myFunction() { ... }' - Never set both
formandlistproperties 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
showMultipleUpdatewithout 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
conditionfor 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.onClickfor server-side scripts —onClickis for client-side logic only. Usescriptproperty for server-side actions. - Never rely on
isolateScriptas 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 theview-rule-guide - For client-side field validation and dynamic behavior → use Client Scripts or UI Policies
- For securing UI Actions by role → see the
rolesproperty and ACL configuration - For programmatic action invocation → use the
actionNameproperty as a reference in scripts - For full API property reference and type signatures → see the
uiaction-apitopic