skills/development/ui-actions/SKILL.md
Complete guide to UI Action development including form buttons, list buttons, context menu actions, client-side and server-side scripts, conditions, security, and common patterns
npx skillsauth add happy-technologies-llc/happy-servicenow-skills ui-actionsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
This skill covers creating and configuring UI Actions in ServiceNow:
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.
admin, personalize (for basic UI actions), or specific application rolesadmin/script-execution for server-side patterns┌─────────────────────────────────────────────────────────────────────────────┐
│ 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] │ │ │ │
│ └─────────────────────────────┘ └─────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
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]
Query the sys_ui_action schema:
Tool: SN-Get-Table-Schema
Parameters:
table_name: sys_ui_action
| 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 |
| 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 |
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.
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"
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
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"
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"
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"
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"
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"
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 |
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"
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"
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"
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"
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"
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"
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');
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"
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;
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"
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"
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"
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"
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"
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"
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"
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
Pre-Testing:
Functional Testing:
Visibility Test:
Client Script Test:
Server Script Test:
Security Test:
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
Download script for local editing:
Tool: SN-Sync-Script-To-Local
Parameters:
script_sys_id: [ui_action_sys_id]
script_field: script
local_path: /Users/developer/servicenow/ui_actions/escalate_incident_server.js
instance: dev
Download onclick script:
Tool: SN-Sync-Script-To-Local
Parameters:
script_sys_id: [ui_action_sys_id]
script_field: onclick
local_path: /Users/developer/servicenow/ui_actions/escalate_incident_client.js
instance: dev
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
| 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 | | Download Script | SN-Sync-Script-To-Local | Local development | | Execute Test Script | SN-Execute-Background-Script | Debug server logic |
Symptom: UI Action button not visible on form/list Causes:
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.
Symptom: Nothing happens when clicking button Causes:
Solution:
client: trueSymptom: Form submits but no action occurs Causes:
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
Symptom: Stays on form or goes to wrong page Causes:
Solution:
action.setRedirectURL(current) at end of server scriptSymptom: Button appears/disappears unexpectedly Causes:
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
admin/script-execution - Background script patternscatalog/ui-policies - Catalog form behavioradmin/workflow-creation - Workflow integrationsecurity/acl-management - Access control listsadmin/script-sync - Local script developmenttesting
Manage supplier onboarding, qualification, performance monitoring, and offboarding with auditable lifecycle controls
tools
Identify emerging risks, prioritize intake signals, and route candidates into formal GRC risk assessment workflows
documentation
Screen inbound documents for completeness, policy risk, and routing readiness before extraction or case workflows
testing
Generate concise task summaries with status, timeline, blockers, SLA risk, and recommended next actions