UI Actions
Overview
This skill covers creating and configuring UI Actions in ServiceNow:
- Form buttons (header/footer) and form context menu items
- List buttons, list context menu, and list choice actions
- Client-side scripts, server-side scripts, and combined execution
- Visibility conditions and role-based security
- Common patterns: Approve, Reject, Clone, Escalate, etc.
- Testing and debugging UI Actions
When to use: When you need to add custom buttons or menu items to forms and lists that execute client-side or server-side logic.
Who should use this: Developers and administrators who need to extend ServiceNow forms and lists with custom functionality.
Prerequisites
- Roles:
admin,personalize(for basic UI actions), or specific application roles - Access: sys_ui_action table, related tables for testing
- Knowledge: JavaScript, GlideRecord API, client-side g_form API
- Related Skills:
admin/script-executionfor server-side patterns
Understanding UI Actions
UI Action Types
┌─────────────────────────────────────────────────────────────────────────────┐
│ UI Action Locations │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────── FORM ────────────┐ ┌─────────── LIST ────────────┐ │
│ │ │ │ │ │
│ │ [Form Button] [Button] │ │ [List Button] [Button] │ │
│ │ ───────────────────────── │ │ ───────────────────────── │ │
│ │ ┌─────────────────────┐ │ │ ☐ Number Description │ │
│ │ │ │ │ │ ☐ INC001 Server down │ │
│ │ │ Form Content │ │ │ ☐ INC002 Email issue │ │
│ │ │ │ │ │ │ │
│ │ │ Right-click → │ │ │ Right-click → │ │
│ │ │ [Context Menu] │ │ │ [Context Menu] │ │
│ │ │ │ │ │ │ │
│ │ └─────────────────────┘ │ │ Actions Dropdown: │ │
│ │ │ │ ☐ List Choice Option │ │
│ │ [Footer Button] [Button] │ │ │ │
│ └─────────────────────────────┘ └─────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
UI Action Execution Flow
User Clicks UI Action
│
▼
┌───────────────────┐
│ Check Conditions │
│ (if defined) │
└────────┬──────────┘
│
Condition
True?
┌───┴───┐
Yes No
│ │
▼ ▼
┌───────┐ [Action
│Client │ Hidden]
│Script?│
└───┬───┘
│
Yes ──► Run Client Script (onclick)
│ │
│ gsftSubmit()?
│ ┌───┴───┐
│ Yes No
│ │ │
│ ▼ ▼
│ ┌───────┐ [Client
│ │Submit │ Only]
│ │ Form │
│ └───┬───┘
│ │
▼ ▼
┌───────────────────┐
│ Run Server Script │◄── No Client Script (direct server)
└───────────────────┘
│
▼
┌─────────┐
│Redirect?│
└────┬────┘
┌───┴───┐
Yes No
│ │
▼ ▼
[Custom [Form
URL] Reload]
Procedure
Phase 1: Understanding UI Action Fields
Step 1.1: Get Table Schema
Query the sys_ui_action schema:
Tool: SN-Get-Table-Schema
Parameters:
table_name: sys_ui_action
Step 1.2: Key Fields Reference
| Field | Type | Description | |-------|------|-------------| | name | String | Button/menu item label | | table | String | Target table (e.g., incident) | | action_name | String | Internal name (no spaces, used in URLs) | | order | Integer | Display order (lower = first) | | active | Boolean | Enable/disable the action | | client | Boolean | Run client-side script (onclick) | | script | Script | Server-side GlideRecord script | | onclick | Script | Client-side JavaScript (g_form) | | condition | Script | Server-side visibility condition | | comments | String | Description/documentation |
Step 1.3: Location Fields
| Field | Type | Description | |-------|------|-------------| | form_button | Boolean | Show as form header button | | form_button_v2 | Boolean | Show as form footer button (v2 forms) | | form_link | Boolean | Show in form context menu | | list_banner_button | Boolean | Show as list header button | | list_button | Boolean | Show in list actions (legacy) | | list_choice | Boolean | Show in list Actions dropdown | | list_context_menu | Boolean | Show in list right-click menu | | list_link | Boolean | Show in list Related Links |
Phase 2: Create Form Button UI Actions
Step 2.1: Basic Form Button
Create a simple form button that runs server-side:
Tool: SN-Create-Record
Parameters:
table_name: sys_ui_action
data:
name: "Escalate to Management"
table: incident
action_name: escalate_to_management
order: 100
active: true
form_button: true
client: false
script: |
// Server-side script
current.escalation = 1;
current.priority = 1;
current.work_notes = "Escalated to management by " + gs.getUserDisplayName();
current.update();
action.setRedirectURL(current);
comments: "Escalates incident to management with P1 priority"
Result: Returns sys_id of created UI action.
Step 2.2: Form Button with Client Script
Create a button with client-side confirmation:
Tool: SN-Create-Record
Parameters:
table_name: sys_ui_action
data:
name: "Close Incident"
table: incident
action_name: close_incident
order: 200
active: true
form_button: true
client: true
onclick: |
// Client-side script
var closeCode = g_form.getValue('close_code');
var closeNotes = g_form.getValue('close_notes');
// Validate required fields
if (!closeCode) {
g_form.showFieldMsg('close_code', 'Close code is required', 'error');
return false; // Prevent submission
}
if (!closeNotes) {
g_form.showFieldMsg('close_notes', 'Close notes are required', 'error');
return false;
}
// Confirm with user
if (!confirm('Are you sure you want to close this incident?')) {
return false;
}
// Submit the form
gsftSubmit(null, g_form.getFormElement(), 'close_incident');
script: |
// Server-side script (runs after form submit)
current.state = 7; // Closed
current.closed_at = new GlideDateTime();
current.closed_by = gs.getUserID();
current.update();
action.setRedirectURL(current);
comments: "Closes incident with validation and confirmation"
Step 2.3: Form Header vs Footer Button
Form Header Button (form_button):
Tool: SN-Create-Record
Parameters:
table_name: sys_ui_action
data:
name: "Quick Assign"
table: incident
action_name: quick_assign
order: 50
active: true
form_button: true
form_button_v2: false
# ... rest of configuration
Form Footer Button (form_button_v2):
Tool: SN-Create-Record
Parameters:
table_name: sys_ui_action
data:
name: "Save and Return"
table: incident
action_name: save_and_return
order: 300
active: true
form_button: false
form_button_v2: true
# ... rest of configuration
Phase 3: Create List UI Actions
Step 3.1: List Banner Button
Button that appears at the top of lists:
Tool: SN-Create-Record
Parameters:
table_name: sys_ui_action
data:
name: "Export Selected"
table: incident
action_name: export_selected
order: 100
active: true
list_banner_button: true
client: true
onclick: |
// Get selected records
var list = GlideList2.get(g_form.getTableName());
var checked = list.getChecked();
if (!checked) {
alert('Please select at least one record');
return false;
}
// Redirect to export with selected sys_ids
var url = 'export_selected.do?sysparm_sys_ids=' + checked;
window.location = url;
comments: "Exports selected incidents to CSV"
Step 3.2: List Context Menu
Right-click menu item for list rows:
Tool: SN-Create-Record
Parameters:
table_name: sys_ui_action
data:
name: "Assign to Me"
table: incident
action_name: assign_to_me_list
order: 100
active: true
list_context_menu: true
client: false
script: |
// Server-side script
// current is the right-clicked record
current.assigned_to = gs.getUserID();
current.assignment_group = gs.getUser().getMyGroups()[0];
current.work_notes = "Self-assigned via list context menu";
current.update();
comments: "Assigns the right-clicked incident to current user"
Step 3.3: List Choice (Actions Dropdown)
Bulk action in the Actions dropdown:
Tool: SN-Create-Record
Parameters:
table_name: sys_ui_action
data:
name: "Bulk Close"
table: incident
action_name: bulk_close
order: 200
active: true
list_choice: true
client: true
onclick: |
// Get checked records
var list = GlideList2.get(g_form.getTableName());
var checked = list.getChecked();
if (!checked) {
alert('Please select at least one incident to close');
return false;
}
var count = checked.split(',').length;
if (!confirm('Close ' + count + ' selected incident(s)?')) {
return false;
}
// Submit to server
gsftSubmit(null, g_form.getFormElement(), 'bulk_close');
script: |
// Server-side - process all checked records
var sysIds = RP.getParameterValue('sysparm_checked_items');
if (sysIds) {
var ids = sysIds.split(',');
for (var i = 0; i < ids.length; i++) {
var gr = new GlideRecord('incident');
if (gr.get(ids[i])) {
gr.state = 7; // Closed
gr.close_code = 'Solved (Permanently)';
gr.close_notes = 'Bulk closed by ' + gs.getUserDisplayName();
gr.update();
}
}
}
comments: "Closes all selected incidents in bulk"
Phase 4: Context Menu (Form Right-Click)
Step 4.1: Form Context Menu Item
Add item to form right-click menu:
Tool: SN-Create-Record
Parameters:
table_name: sys_ui_action
data:
name: "View Audit History"
table: incident
action_name: view_audit_history
order: 500
active: true
form_link: true
client: true
onclick: |
// Open audit history in new window
var sysId = g_form.getUniqueValue();
var tableName = g_form.getTableName();
var url = 'sys_audit_list.do?sysparm_query=tablename=' + tableName +
'^documentkey=' + sysId + '&sysparm_view=';
window.open(url, '_blank');
comments: "Opens audit history for current record in new tab"
Step 4.2: Form Link with Server Script
Context menu that runs server-side:
Tool: SN-Create-Record
Parameters:
table_name: sys_ui_action
data:
name: "Create Child Incident"
table: incident
action_name: create_child_incident
order: 600
active: true
form_link: true
client: false
script: |
// Create child incident
var child = new GlideRecord('incident');
child.initialize();
child.parent_incident = current.sys_id;
child.short_description = 'Child of: ' + current.short_description;
child.description = 'Child incident created from ' + current.number;
child.caller_id = current.caller_id;
child.category = current.category;
child.subcategory = current.subcategory;
child.priority = current.priority;
var childId = child.insert();
// Redirect to new child incident
action.setRedirectURL(child);
comments: "Creates a child incident linked to current record"
Phase 5: Conditions for Visibility
Step 5.1: Simple Field Conditions
Show button only for specific states:
Tool: SN-Create-Record
Parameters:
table_name: sys_ui_action
data:
name: "Reopen Incident"
table: incident
action_name: reopen_incident
order: 100
active: true
form_button: true
condition: "current.state == 7"
script: |
// Reopen logic
current.state = 2; // In Progress
current.work_notes = "Reopened by " + gs.getUserDisplayName();
current.update();
action.setRedirectURL(current);
comments: "Visible only when incident is Closed (state=7)"
Common Condition Patterns:
| Condition | Meaning |
|-----------|---------|
| current.state == 1 | State equals New |
| current.active == true | Record is active |
| current.assigned_to == gs.getUserID() | Assigned to current user |
| current.assignment_group.manager == gs.getUserID() | Current user is group manager |
| current.isNewRecord() | New record (not saved yet) |
| !current.isNewRecord() | Existing record only |
| current.priority <= 2 | Priority is 1 or 2 |
| current.caller_id.vip == true | Caller is VIP |
Step 5.2: Role-Based Conditions
Show button only for specific roles:
Tool: SN-Create-Record
Parameters:
table_name: sys_ui_action
data:
name: "Override SLA"
table: incident
action_name: override_sla
order: 100
active: true
form_button: true
condition: "gs.hasRole('incident_manager') || gs.hasRole('admin')"
script: |
// SLA override logic
current.sla_due = '';
current.work_notes = "SLA overridden by manager";
current.update();
comments: "Visible only to incident managers and admins"
Step 5.3: Complex Conditions
Multiple conditions combined:
Tool: SN-Create-Record
Parameters:
table_name: sys_ui_action
data:
name: "Emergency Escalation"
table: incident
action_name: emergency_escalation
order: 50
active: true
form_button: true
condition: |
// Only show for P1/P2 active incidents assigned to current user's group
current.state != 6 && current.state != 7 &&
current.priority <= 2 &&
current.assignment_group.getDisplayValue() != '' &&
gs.getUser().isMemberOf(current.assignment_group)
script: |
// Emergency escalation logic
current.escalation = 3;
current.work_notes = "[EMERGENCY ESCALATION] " + gs.getUserDisplayName();
current.update();
comments: "Emergency escalation for P1/P2 incidents in user's group"
Step 5.4: UI Action Visibility Best Practices
Tool: SN-Create-Record
Parameters:
table_name: sys_ui_action
data:
name: "Approve Request"
table: sc_req_item
action_name: approve_request
order: 100
active: true
form_button: true
condition: |
// Best practice: Check multiple conditions
var canApprove = false;
// 1. Check record state
if (current.approval == 'requested') {
// 2. Check if user is an approver
var approver = new GlideRecord('sysapproval_approver');
approver.addQuery('sysapproval', current.sys_id);
approver.addQuery('approver', gs.getUserID());
approver.addQuery('state', 'requested');
approver.query();
if (approver.next()) {
canApprove = true;
}
// 3. Or check if user has override role
if (gs.hasRole('approval_admin')) {
canApprove = true;
}
}
canApprove;
script: |
// Approval logic
comments: "Visible only when user can approve this request"
Phase 6: Client-Side vs Server-Side Scripts
Step 6.1: Client-Side Only (No Submit)
Action that only runs client-side:
Tool: SN-Create-Record
Parameters:
table_name: sys_ui_action
data:
name: "Copy to Clipboard"
table: incident
action_name: copy_to_clipboard
order: 100
active: true
form_button: true
client: true
onclick: |
// Pure client-side - no server round trip
var number = g_form.getValue('number');
var shortDesc = g_form.getValue('short_description');
var text = number + ': ' + shortDesc;
// Copy to clipboard (modern browsers)
navigator.clipboard.writeText(text).then(function() {
g_form.addInfoMessage('Copied to clipboard: ' + text);
}).catch(function(err) {
alert('Failed to copy: ' + err);
});
// Return false to prevent form submission
return false;
script: ""
comments: "Copies incident number and description to clipboard"
Step 6.2: Server-Side Only
Action that only runs server-side:
Tool: SN-Create-Record
Parameters:
table_name: sys_ui_action
data:
name: "Recalculate Priority"
table: incident
action_name: recalculate_priority
order: 100
active: true
form_button: true
client: false
script: |
// Server-side only
var calculator = new PriorityCalculator();
var newPriority = calculator.calculate(current.impact, current.urgency);
current.priority = newPriority;
current.work_notes = "Priority recalculated to " + newPriority;
current.update();
action.setRedirectURL(current);
comments: "Recalculates priority based on impact and urgency matrix"
Step 6.3: Combined Client and Server
Client validates/confirms, then server executes:
Tool: SN-Create-Record
Parameters:
table_name: sys_ui_action
data:
name: "Transfer to Team"
table: incident
action_name: transfer_to_team
order: 100
active: true
form_button: true
client: true
onclick: |
// Client-side validation and data collection
var group = g_form.getValue('assignment_group');
if (!group) {
g_form.showFieldMsg('assignment_group', 'Please select a group first', 'error');
return false;
}
var reason = prompt('Enter transfer reason (required):');
if (!reason || reason.trim() === '') {
alert('Transfer reason is required');
return false;
}
// Store reason in a hidden field or use sysparm
g_form.setValue('work_notes', 'Transfer Reason: ' + reason);
// Submit to server
gsftSubmit(null, g_form.getFormElement(), 'transfer_to_team');
script: |
// Server-side execution
var targetGroup = current.assignment_group.getDisplayValue();
// Clear individual assignee
current.assigned_to = '';
// Add transfer audit trail
current.work_notes = 'Transferred to ' + targetGroup + '\n' +
'Transferred by: ' + gs.getUserDisplayName();
current.update();
gs.addInfoMessage('Incident transferred to ' + targetGroup);
action.setRedirectURL(current);
comments: "Transfers incident with reason capture"
Phase 7: Using g_form in UI Actions
Step 7.1: Common g_form Methods
Reading Field Values:
// In onclick script
var value = g_form.getValue('field_name');
var display = g_form.getDisplayBox('field_name').value;
var reference = g_form.getReference('caller_id', function(ref) {
// Async callback for reference fields
alert('Caller name: ' + ref.name);
});
Setting Field Values:
// In onclick script
g_form.setValue('priority', '1');
g_form.setValue('work_notes', 'Updated via UI Action');
g_form.setDisplay('field_name', true); // Show field
g_form.setMandatory('field_name', true); // Make mandatory
g_form.setReadOnly('field_name', true); // Make read-only
User Feedback:
// Messages
g_form.addInfoMessage('Operation completed successfully');
g_form.addWarningMessage('Please review the changes');
g_form.addErrorMessage('An error occurred');
g_form.showFieldMsg('field_name', 'Message text', 'info|warning|error');
// Clear messages
g_form.clearMessages();
g_form.hideFieldMsg('field_name');
Step 7.2: Complete Client Script Example
Tool: SN-Create-Record
Parameters:
table_name: sys_ui_action
data:
name: "Validate and Submit"
table: incident
action_name: validate_submit
order: 100
active: true
form_button: true
client: true
onclick: |
function validateAndSubmit() {
// Clear previous messages
g_form.clearMessages();
var valid = true;
var errors = [];
// Validate short description
var shortDesc = g_form.getValue('short_description');
if (!shortDesc || shortDesc.length < 10) {
g_form.showFieldMsg('short_description', 'Must be at least 10 characters', 'error');
valid = false;
errors.push('Short description');
}
// Validate caller
var caller = g_form.getValue('caller_id');
if (!caller) {
g_form.showFieldMsg('caller_id', 'Caller is required', 'error');
valid = false;
errors.push('Caller');
}
// Validate category
var category = g_form.getValue('category');
if (!category) {
g_form.showFieldMsg('category', 'Category is required', 'error');
valid = false;
errors.push('Category');
}
// Check P1 requirements
var priority = g_form.getValue('priority');
if (priority == '1') {
var businessImpact = g_form.getValue('business_impact');
if (!businessImpact) {
g_form.showFieldMsg('business_impact', 'Required for P1 incidents', 'error');
valid = false;
errors.push('Business Impact');
}
}
if (!valid) {
g_form.addErrorMessage('Please fix validation errors: ' + errors.join(', '));
return false;
}
// Confirmation
if (confirm('Submit this incident?')) {
gsftSubmit(null, g_form.getFormElement(), 'validate_submit');
return true;
}
return false;
}
validateAndSubmit();
script: |
current.update();
gs.addInfoMessage('Incident saved successfully');
action.setRedirectURL(current);
comments: "Validates required fields before submission"
Phase 8: Redirecting After Action
Step 8.1: Redirect Methods
Redirect to Same Record:
// Server-side script
action.setRedirectURL(current);
Redirect to Different Record:
// Server-side script
var newRecord = new GlideRecord('incident');
newRecord.get('some_sys_id');
action.setRedirectURL(newRecord);
Redirect to List:
// Server-side script
action.setRedirectURL('incident_list.do?sysparm_query=active=true');
Redirect to Specific URL:
// Server-side script
action.setRedirectURL('/nav_to.do?uri=incident.do?sys_id=' + current.sys_id);
Prevent Redirect (Stay on Form):
// Server-side script
current.update();
action.setRedirectURL(current); // Refresh current record
Redirect to Previous Page:
// Client-side onclick
// After server processing, redirect back
history.back();
return false;
Step 8.2: Common Redirect Patterns
Clone and Redirect to New:
Tool: SN-Create-Record
Parameters:
table_name: sys_ui_action
data:
name: "Clone Incident"
table: incident
action_name: clone_incident
order: 100
active: true
form_button: true
condition: "!current.isNewRecord()"
client: false
script: |
// Clone the incident
var clone = new GlideRecord('incident');
clone.initialize();
// Copy fields
clone.short_description = 'CLONE: ' + current.short_description;
clone.description = current.description;
clone.caller_id = current.caller_id;
clone.category = current.category;
clone.subcategory = current.subcategory;
clone.priority = current.priority;
clone.impact = current.impact;
clone.urgency = current.urgency;
// Reference original
clone.work_notes = 'Cloned from ' + current.number;
var cloneId = clone.insert();
// Redirect to new cloned record
action.setRedirectURL(clone);
comments: "Creates a copy of the current incident"
Save and Return to List:
Tool: SN-Create-Record
Parameters:
table_name: sys_ui_action
data:
name: "Save and Return to List"
table: incident
action_name: save_return_list
order: 100
active: true
form_button: true
client: false
script: |
current.update();
action.setRedirectURL('incident_list.do');
comments: "Saves current record and returns to incident list"
Phase 9: Action Security
Step 9.1: Role-Based Access
Restrict to Specific Roles (Using Condition):
Tool: SN-Create-Record
Parameters:
table_name: sys_ui_action
data:
name: "Delete Incident"
table: incident
action_name: delete_incident
order: 900
active: true
form_button: true
condition: "gs.hasRole('incident_admin')"
client: true
onclick: |
if (!confirm('Are you sure you want to DELETE this incident? This cannot be undone.')) {
return false;
}
if (!confirm('FINAL WARNING: This will permanently delete ' + g_form.getValue('number'))) {
return false;
}
gsftSubmit(null, g_form.getFormElement(), 'delete_incident');
script: |
var number = current.number;
current.deleteRecord();
gs.addInfoMessage('Incident ' + number + ' has been deleted');
action.setRedirectURL('incident_list.do');
comments: "Permanently deletes incident - restricted to incident_admin"
Step 9.2: Create ACL for UI Action
Create explicit ACL protection:
Tool: SN-Create-Record
Parameters:
table_name: sys_security_acl
data:
name: "incident.delete_incident"
type: ui_action
operation: execute
active: true
admin_overrides: false
advanced: false
script: "gs.hasRole('incident_admin')"
description: "ACL for delete incident UI action"
Step 9.3: Security Best Practices
- Always validate on server-side: Client-side checks can be bypassed
- Use conditions AND ACLs: Defense in depth
- Log sensitive actions: Create audit trail
- Limit destructive actions: Require confirmation and restrict roles
Complete Secure Action Example:
Tool: SN-Create-Record
Parameters:
table_name: sys_ui_action
data:
name: "Override Approval"
table: sc_req_item
action_name: override_approval
order: 100
active: true
form_button: true
condition: |
// Check role and record state
gs.hasRole('approval_admin') &&
current.approval == 'requested' &&
!current.isNewRecord()
client: true
onclick: |
// Client-side confirmation
var reason = prompt('Enter override reason (required for audit):');
if (!reason || reason.trim().length < 10) {
alert('A detailed reason (min 10 chars) is required for approval override');
return false;
}
if (!confirm('Override approval for ' + g_form.getValue('number') + '?')) {
return false;
}
// Store reason
g_form.setValue('work_notes', 'APPROVAL OVERRIDE REASON: ' + reason);
gsftSubmit(null, g_form.getFormElement(), 'override_approval');
script: |
// Server-side validation (defense in depth)
if (!gs.hasRole('approval_admin')) {
gs.addErrorMessage('Unauthorized: approval_admin role required');
action.setRedirectURL(current);
return;
}
// Process override
current.approval = 'approved';
current.work_notes = 'APPROVAL OVERRIDE\n' +
'Overridden by: ' + gs.getUserDisplayName() + '\n' +
'Date: ' + new GlideDateTime().getDisplayValue();
current.update();
// Audit log
gs.log('Approval override on ' + current.number + ' by ' + gs.getUserName(), 'ApprovalOverride');
gs.addInfoMessage('Approval overridden successfully');
action.setRedirectURL(current);
comments: "Override approval with audit trail - approval_admin only"
Phase 10: Common Use Cases
Step 10.1: Approve/Reject Pattern
Approve Button:
Tool: SN-Create-Record
Parameters:
table_name: sys_ui_action
data:
name: "Approve"
table: sc_req_item
action_name: approve_item
order: 100
active: true
form_button: true
condition: |
current.approval == 'requested' &&
new ApprovalHelper().isUserApprover(gs.getUserID(), current.sys_id)
client: true
onclick: |
if (!confirm('Approve this request?')) {
return false;
}
gsftSubmit(null, g_form.getFormElement(), 'approve_item');
script: |
var approver = new GlideRecord('sysapproval_approver');
approver.addQuery('sysapproval', current.sys_id);
approver.addQuery('approver', gs.getUserID());
approver.addQuery('state', 'requested');
approver.query();
if (approver.next()) {
approver.state = 'approved';
approver.update();
current.work_notes = 'Approved by ' + gs.getUserDisplayName();
current.update();
gs.addInfoMessage('Request approved');
}
action.setRedirectURL(current);
comments: "Approve request item"
Reject Button:
Tool: SN-Create-Record
Parameters:
table_name: sys_ui_action
data:
name: "Reject"
table: sc_req_item
action_name: reject_item
order: 110
active: true
form_button: true
condition: |
current.approval == 'requested' &&
new ApprovalHelper().isUserApprover(gs.getUserID(), current.sys_id)
client: true
onclick: |
var reason = prompt('Enter rejection reason (required):');
if (!reason || reason.trim() === '') {
alert('Rejection reason is required');
return false;
}
g_form.setValue('work_notes', 'REJECTION REASON: ' + reason);
if (!confirm('Reject this request?')) {
return false;
}
gsftSubmit(null, g_form.getFormElement(), 'reject_item');
script: |
var approver = new GlideRecord('sysapproval_approver');
approver.addQuery('sysapproval', current.sys_id);
approver.addQuery('approver', gs.getUserID());
approver.addQuery('state', 'requested');
approver.query();
if (approver.next()) {
approver.state = 'rejected';
approver.update();
current.work_notes = 'Rejected by ' + gs.getUserDisplayName();
current.update();
gs.addInfoMessage('Request rejected');
}
action.setRedirectURL(current);
comments: "Reject request item with reason"
Step 10.2: Escalation Pattern
Tool: SN-Create-Record
Parameters:
table_name: sys_ui_action
data:
name: "Escalate"
table: incident
action_name: escalate_incident
order: 100
active: true
form_button: true
condition: "current.escalation < 3 && current.state != 6 && current.state != 7"
client: true
onclick: |
var currentLevel = parseInt(g_form.getValue('escalation')) || 0;
var levels = ['Normal', 'Moderate', 'High', 'Overdue'];
var nextLevel = levels[currentLevel + 1] || 'Maximum';
if (!confirm('Escalate to ' + nextLevel + ' level?')) {
return false;
}
gsftSubmit(null, g_form.getFormElement(), 'escalate_incident');
script: |
var oldLevel = current.escalation;
current.escalation = parseInt(current.escalation) + 1;
current.work_notes = 'Escalated from level ' + oldLevel + ' to ' + current.escalation +
' by ' + gs.getUserDisplayName();
current.update();
gs.addInfoMessage('Incident escalated to level ' + current.escalation);
action.setRedirectURL(current);
comments: "Escalates incident to next level"
Step 10.3: Clone Pattern
Tool: SN-Create-Record
Parameters:
table_name: sys_ui_action
data:
name: "Clone Record"
table: incident
action_name: clone_record
order: 200
active: true
form_link: true
condition: "!current.isNewRecord()"
client: false
script: |
// Fields to copy
var copyFields = [
'short_description', 'description', 'caller_id',
'category', 'subcategory', 'priority', 'impact',
'urgency', 'assignment_group', 'contact_type'
];
// Fields to exclude (system fields, unique identifiers)
var excludeFields = [
'sys_id', 'number', 'opened_at', 'opened_by',
'closed_at', 'closed_by', 'resolved_at', 'resolved_by',
'sys_created_on', 'sys_created_by', 'sys_updated_on', 'sys_updated_by'
];
var clone = new GlideRecord('incident');
clone.initialize();
// Copy specified fields
for (var i = 0; i < copyFields.length; i++) {
var field = copyFields[i];
if (current.getValue(field)) {
clone.setValue(field, current.getValue(field));
}
}
// Modify for clone
clone.short_description = '[CLONE] ' + clone.short_description;
clone.work_notes = 'Cloned from ' + current.number + ' by ' + gs.getUserDisplayName();
var sysId = clone.insert();
gs.addInfoMessage('Cloned incident ' + current.number + ' successfully');
action.setRedirectURL(clone);
comments: "Creates a copy of the incident"
Phase 11: Testing UI Actions
Step 11.1: Query Existing UI Actions
Find all UI actions for a table:
Tool: SN-Query-Table
Parameters:
table_name: sys_ui_action
query: table=incident^active=true
fields: sys_id,name,action_name,order,form_button,list_banner_button,condition
limit: 50
Find UI actions by name:
Tool: SN-Query-Table
Parameters:
table_name: sys_ui_action
query: nameLIKEapprove^active=true
fields: sys_id,name,table,condition,client
limit: 20
Step 11.2: Testing Checklist
Pre-Testing:
- [ ] UI Action is active
- [ ] Table is correct
- [ ] Location is set (form_button, list_banner_button, etc.)
- [ ] Condition returns true for test scenario
- [ ] Scripts have no syntax errors
Functional Testing:
-
Visibility Test:
- Does button appear when condition is true?
- Does button hide when condition is false?
- Is button visible to correct roles only?
-
Client Script Test:
- Does onclick validation work?
- Are error messages displayed?
- Does confirmation dialog appear?
- Does form submit correctly?
-
Server Script Test:
- Is record updated correctly?
- Is redirect working?
- Are work notes added?
- Is audit trail created?
-
Security Test:
- Can unauthorized users access the action?
- Can users bypass client validation?
- Are server-side role checks in place?
Step 11.3: Debug Server Script
Add Debug Logging:
Tool: SN-Update-Record
Parameters:
table_name: sys_ui_action
sys_id: [action_sys_id]
data:
script: |
// Debug logging
gs.info('[DEBUG] UI Action: escalate_incident started');
gs.info('[DEBUG] Current user: ' + gs.getUserName());
gs.info('[DEBUG] Current record: ' + current.number);
try {
// Your logic here
current.escalation = parseInt(current.escalation) + 1;
current.update();
gs.info('[DEBUG] Escalation updated to: ' + current.escalation);
} catch (e) {
gs.error('[DEBUG] Error: ' + e.message);
}
action.setRedirectURL(current);
Check Logs:
Tool: SN-Query-Table
Parameters:
table_name: syslog
query: messageLIKE[DEBUG] UI Action^sys_created_on>javascript:gs.minutesAgo(10)
fields: message,level,sys_created_on
limit: 50
Phase 12: Explicit Local Script Development
Step 12.1: Pull UI Action Fields Locally
Retrieve both script fields and freshness metadata:
Tool: SN-Get-Record
Parameters:
table_name: sys_ui_action
sys_id: [ui_action_sys_id]
fields: sys_id,script,onclick,sys_updated_on,sys_mod_count
instance: dev
Write script and onclick to separate local files. Before pushing, retrieve
the same fields again and stop if sys_updated_on, sys_mod_count, or either
remote field changed. After merging any conflict locally, update only the
reviewed fields:
Tool: SN-Update-Record
Parameters:
table_name: sys_ui_action
sys_id: [ui_action_sys_id]
data:
script: [reviewed_server_script]
onclick: [reviewed_client_script]
instance: dev
Step 12.2: Directory Structure
servicenow/
└── ui_actions/
├── incident/
│ ├── escalate_incident_server.js
│ ├── escalate_incident_client.js
│ ├── close_incident_server.js
│ └── close_incident_client.js
├── sc_req_item/
│ ├── approve_item_server.js
│ └── reject_item_server.js
└── README.md
Tool Usage Summary
| Operation | MCP Tool | Notes | |-----------|----------|-------| | Create UI Action | SN-Create-Record | sys_ui_action table | | Update UI Action | SN-Update-Record | Modify script, condition | | Query UI Actions | SN-Query-Table | Find by table, name | | Get Schema | SN-Get-Table-Schema | Understand available fields | | Local script cycle | SN-Get-Record + SN-Update-Record | Pull, freshness check, edit, push | | Execute Test Script | SN-Execute-Background-Script | Debug server logic |
Best Practices
- Descriptive Names: Use clear, action-oriented names (e.g., "Escalate to Management")
- Consistent Order: Use standardized order numbers (100, 200, 300)
- Always Validate: Both client-side (user experience) and server-side (security)
- Use Conditions: Hide irrelevant buttons instead of showing disabled ones
- Add Comments: Document the purpose and any dependencies
- Role-Based Access: Always restrict sensitive actions
- Audit Trail: Add work notes for all significant actions
- Error Handling: Wrap server scripts in try-catch blocks
- Test Thoroughly: Test all conditions, roles, and edge cases
- Version Control: Sync scripts locally for version control
Troubleshooting
Button Not Appearing
Symptom: UI Action button not visible on form/list Causes:
- UI Action is inactive
- Condition evaluates to false
- Wrong location flag set
- User lacks required role
Solution:
Tool: SN-Query-Table
Parameters:
table_name: sys_ui_action
query: action_name=my_action
fields: active,condition,form_button,table
Check active=true and verify condition logic.
Client Script Not Executing
Symptom: Nothing happens when clicking button Causes:
- JavaScript syntax error
- client field not set to true
- onclick is empty
Solution:
- Check browser console (F12) for errors
- Verify
client: true - Test onclick script in browser console
Server Script Failing
Symptom: Form submits but no action occurs Causes:
- Server script syntax error
- Permission error
- Missing current.update()
Solution:
Tool: SN-Query-Table
Parameters:
table_name: syslog
query: levelINerror,warn^sys_created_on>javascript:gs.minutesAgo(5)
fields: message,source,sys_created_on
limit: 20
Redirect Not Working
Symptom: Stays on form or goes to wrong page Causes:
- Missing action.setRedirectURL()
- Invalid redirect target
- Client returning false before submit
Solution:
- Verify
action.setRedirectURL(current)at end of server script - For custom URLs, ensure valid format
- Check client onclick returns true or submits form
Condition Errors
Symptom: Button appears/disappears unexpectedly Causes:
- Condition syntax error (fails silently)
- Reference field not loaded
- Type mismatch (string vs number)
Solution: Test condition in background script:
Tool: SN-Execute-Background-Script
Parameters:
script: |
var current = new GlideRecord('incident');
current.get('[sys_id]');
// Test your condition
var result = current.state == 7; // Your condition
gs.info('Condition result: ' + result);
description: Test UI action condition
Related Skills
admin/script-execution- Background script patternscatalog/ui-policies- Catalog form behavioradmin/workflow-creation- Workflow integrationsecurity/acl-management- Access control listsadmin/script-sync- Local script development
Scan to join WeChat group