关于 ui-actions
ui-actions 是面向 ServiceNow 开发者和管理员的技能,指导创建和配置表单及列表 UI Action。内容涵盖脚本执行、可见性条件、角色安全、测试与调试。
主要功能
- 01配置表单按钮和菜单项
- 02创建列表操作和批量操作
- 03设置客户端与服务器脚本
- 04配置可见性条件和角色限制
- 05测试与调试 UI Action
适用场景
- 01为表单添加自定义操作按钮
- 02为列表创建上下文菜单或批量操作
- 03按状态或角色控制操作可见性
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ happy-technologies-llc/ happy-platform-skills --skill ui-actions安装后在 Claude Code 中输入/ui-actions确认已加载。
详细介绍
UI 操作
概述
本技能介绍如何在 ServiceNow 中创建和配置 UI 操作:
- 表单按钮(页眉/页脚)和表单上下文菜单项
- 列表按钮、列表上下文菜单和列表选择操作
- 客户端脚本、服务器端脚本和组合执行
- 可见性条件和基于角色的安全性
- 常见模式:批准、拒绝、克隆、升级等
- 测试和调试 UI 操作
适用场景: 需要向表单和列表添加可执行客户端或服务器端逻辑的自定义按钮或菜单项时。
适用对象: 需要通过自定义功能扩展 ServiceNow 表单和列表的开发者和管理员。
前提条件
- 角色:
admin、personalize(用于基本 UI 操作)或特定应用角色 - 访问权限: sys_ui_action 表,以及用于测试的相关表
- 知识: JavaScript、GlideRecord API、客户端 g_form API
- 相关技能: 用于服务器端模式的
admin/script-execution
了解 UI 操作
UI 操作类型
┌─────────────────────────────────────────────────────────────────────────────┐
│ 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 操作执行流程
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]
操作步骤
阶段 1:了解 UI 操作字段
步骤 1.1:获取表架构
查询 sys_ui_action 架构:
Tool: SN-Get-Table-Schema
Parameters:
table_name: sys_ui_action
步骤 1.2:关键字段参考
| 字段 | 类型 | 说明 |
|---|---|---|
| name | 字符串 | 按钮/菜单项标签 |
| table | 字符串 | 目标表(例如 incident) |
| action_name | 字符串 | 内部名称(不含空格,用于 URL) |
| order | 整数 | 显示顺序(数值越小,越靠前) |
| active | 布尔值 | 启用/禁用此操作 |
| client | 布尔值 | 运行客户端脚本(onclick) |
| script | 脚本 | 服务器端 GlideRecord 脚本 |
| onclick | 脚本 | 客户端 JavaScript(g_form) |
| condition | 脚本 | 服务器端可见性条件 |
| comments | 字符串 | 说明/文档 |
步骤 1.3:位置字段
| 字段 | 类型 | 说明 |
|---|---|---|
| form_button | 布尔值 | 显示为表单页眉按钮 |
| form_button_v2 | 布尔值 | 显示为表单页脚按钮(v2 表单) |
| form_link | 布尔值 | 显示在表单上下文菜单中 |
| list_banner_button | 布尔值 | 显示为列表页眉按钮 |
| list_button | 布尔值 | 显示在列表操作中(旧版) |
| list_choice | 布尔值 | 显示在列表操作下拉菜单中 |
| list_context_menu | 布尔值 | 显示在列表右键菜单中 |
| list_link | 布尔值 | 显示在列表相关链接中 |
阶段 2:创建表单按钮 UI 操作
步骤 2.1:基本表单按钮
创建一个在服务器端运行的简单表单按钮:
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"
结果: 返回所创建 UI 操作的 sys_id。
步骤 2.2:带客户端脚本的表单按钮
创建带客户端确认的按钮:
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"
步骤 2.3:表单页眉按钮与页脚按钮
表单页眉按钮(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_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
阶段 3:创建列表 UI 操作
步骤 3.1:列表横幅按钮
显示在列表顶部的按钮:
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"
步骤 3.2:列表上下文菜单
列表行的右键菜单项:
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"
步骤 3.3:列表选择操作(操作下拉菜单)
操作下拉菜单中的批量操作:
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"
阶段 4:上下文菜单(表单右键菜单)
步骤 4.1:表单上下文菜单项
向表单右键菜单添加菜单项:
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"
步骤 4.2:带服务器端脚本的表单链接
在服务器端运行的上下文菜单:
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"
阶段 5:可见性条件
步骤 5.1:简单条件
仅在特定状态下显示按钮:
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)"
常见条件模式:
| 条件 | 含义 |
|---|---|
current.state == 1 | 状态为 New |
current.active == true | 记录处于活动状态 |
current.assigned_to == gs.getUserID() | 分配给当前用户 |
current.assignment_group.manager == gs.getUserID() | 当前用户是组经理 |
current.isNewRecord() | 新记录(尚未保存) |
!current.isNewRecord() | 仅限现有记录 |
current.priority <= 2 | 优先级为 1 或 2 |
current.caller_id.vip == true | 呼叫者是 VIP |
步骤 5.2:基于角色的条件
仅对特定角色显示按钮:
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"
步骤 5.3:复杂条件
组合多个条件:
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"
步骤 5.4:UI Action 可见性最佳实践
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"
阶段 6:客户端脚本与服务器端脚本
步骤 6.1:仅客户端(不提交)
仅在客户端运行的操作:
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"
步骤 6.2:仅服务器端
仅在服务器端运行的操作:
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"
步骤 6.3:组合客户端和服务器端
客户端验证/确认,然后由服务器执行:
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"
阶段 7:在 UI Actions 中使用 g_form
步骤 7.1:常见 g_form 方法
读取字段值:
// 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);
});
设置字段值:
// 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
用户反馈:
// 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');
步骤 7.2:完整客户端脚本示例
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"
阶段 8:操作后重定向
步骤 8.1:重定向方法
重定向到同一条记录:
// Server-side script
action.setRedirectURL(current);
重定向到另一条记录:
// Server-side script
var newRecord = new GlideRecord('incident');
newRecord.get('some_sys_id');
action.setRedirectURL(newRecord);
重定向到列表:
// Server-side script
action.setRedirectURL('incident_list.do?sysparm_query=active=true');
重定向到特定 URL:
// Server-side script
action.setRedirectURL('/nav_to.do?uri=incident.do?sys_id=' + current.sys_id);
阻止重定向(留在表单上):
// Server-side script
current.update();
action.setRedirectURL(current); // Refresh current record
重定向到上一页:
// Client-side onclick
// After server processing, redirect back
history.back();
return false;
步骤 8.2:常见重定向模式
克隆并重定向到新记录:
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"
保存并返回列表:
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"
阶段 9:操作安全
步骤 9.1:基于角色的访问控制
限制特定角色使用(使用条件):
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"
步骤 9.2:为 UI Action 创建 ACL
创建明确的 ACL 保护:
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"
步骤 9.3:安全最佳实践
- 始终在服务器端进行验证: 客户端检查可能被绕过
- 同时使用条件和 ACL: 纵深防御
- 记录敏感操作: 创建审计跟踪
- 限制破坏性操作: 要求确认并限制角色
完整的安全操作示例:
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"
阶段 10:常见用例
步骤 10.1:批准/拒绝模式
批准按钮:
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"
拒绝按钮:
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"
步骤 10.2:升级模式
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"
步骤 10.3:克隆模式
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"
阶段 11:测试 UI Actions
步骤 11.1:查询现有 UI Actions
查找表中的所有 UI Actions:
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
按名称查找 UI Actions:
Tool: SN-Query-Table
Parameters:
table_name: sys_ui_action
query: nameLIKEapprove^active=true
fields: sys_id,name,table,condition,client
limit: 20
步骤 11.2:测试清单
测试前:
- UI Action 已启用
- 表正确
- 已设置位置(form_button、list_banner_button 等)
- 在测试场景下条件返回 true
- 脚本没有语法错误
功能测试:
-
可见性测试:
- 条件为 true 时按钮是否显示?
- 条件为 false 时按钮是否隐藏?
- 按钮是否仅对正确的角色可见?
-
客户端脚本测试:
- onclick 验证是否正常工作?
- 是否显示错误消息?
- 是否显示确认对话框?
- 表单是否正确提交?
-
服务器端脚本测试:
- 记录是否正确更新?
- 重定向是否正常工作?
- 是否添加了工作备注?
- 是否创建了审计跟踪?
-
安全测试:
- 未授权用户能否访问该操作?
- 用户能否绕过客户端验证?
- 是否设置了服务器端角色检查?
步骤 11.3:调试服务器端脚本
添加调试日志:
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);
检查日志:
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
阶段 12:显式本地脚本开发
步骤 12.1:在本地拉取 UI Action 字段
同时获取脚本字段和新鲜度元数据:
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
将 script 和 onclick 分别写入本地文件。推送前,再次获取相同字段;如果 sys_updated_on、sys_mod_count 或任一远程字段发生变化,则停止操作。在本地合并任何冲突后,只更新已审核的字段:
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
步骤 12.2:目录结构
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
工具使用摘要
| 操作 | MCP 工具 | 说明 |
|---|---|---|
| 创建 UI Action | SN-Create-Record | sys_ui_action 表 |
| 更新 UI Action | SN-Update-Record | 修改 script、condition |
| 查询 UI Action | SN-Query-Table | 按表、名称查找 |
| 获取架构 | SN-Get-Table-Schema | 了解可用字段 |
| 本地脚本周期 | SN-Get-Record + SN-Update-Record | 拉取、检查新鲜度、编辑、推送 |
| 执行测试脚本 | SN-Execute-Background-Script | 调试服务器逻辑 |
最佳实践
- 使用描述性名称: 使用清晰、以动作为导向的名称(例如,“升级至管理层”)
- 保持顺序一致: 使用标准化的顺序编号(100、200、300)
- 始终进行验证: 同时验证客户端(用户体验)和服务器端(安全性)
- 使用条件: 隐藏不相关的按钮,而不是显示禁用的按钮
- 添加注释: 记录用途和任何依赖项
- 基于角色的访问: 始终限制敏感操作
- 审计跟踪: 为所有重要操作添加工作备注
- 错误处理: 使用 try-catch 块封装服务器脚本
- 全面测试: 测试所有条件、角色和边缘情况
- 版本控制: 在本地同步脚本以进行版本控制
故障排除
按钮未显示
症状: UI Action 按钮未在表单/列表上显示 原因:
- UI Action 未激活
- 条件计算结果为 false
- 设置了错误的位置标志
- 用户缺少所需角色
解决方案:
Tool: SN-Query-Table
Parameters:
table_name: sys_ui_action
query: action_name=my_action
fields: active,condition,form_button,table
检查 active=true,并验证条件逻辑。
客户端脚本未执行
症状: 点击按钮时没有任何反应 原因:
- JavaScript 语法错误
- client 字段未设为 true
- onclick 为空
解决方案:
- 检查浏览器控制台(F12)中的错误
- 验证
client: true - 在浏览器控制台中测试 onclick 脚本
服务器脚本失败
症状: 表单已提交,但没有执行任何操作 原因:
- 服务器脚本语法错误
- 权限错误
- 缺少 current.update()
解决方案:
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
重定向无效
症状: 停留在表单页面或跳转到错误页面 原因:
- 缺少 action.setRedirectURL()
- 重定向目标无效
- 客户端在提交前返回 false
解决方案:
- 验证服务器脚本末尾是否有
action.setRedirectURL(current) - 对于自定义 URL,确保格式有效
- 检查客户端 onclick 是否返回 true 或提交表单
条件错误
症状: 按钮意外显示/消失 原因:
- 条件语法错误(不会显示错误)
- 未加载引用字段
- 类型不匹配(字符串与数字)
解决方案: 在后台脚本中测试条件:
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- 后台脚本模式catalog/ui-policies- 目录表单行为admin/workflow-creation- 工作流集成security/acl-management- 访问控制列表admin/script-sync- 本地脚本开发