ui-actions について
ui-actions は、ServiceNow の開発者や管理者向けに、フォームやリストの UI Action の作成・設定を案内するスキルです。スクリプト、表示条件、ロールベースのセキュリティ、テスト、デバッグを扱います。
主な機能
- 01フォームボタンとメニュー項目の設定
- 02リスト操作と一括操作の作成
- 03クライアント側・サーバー側スクリプトの設定
- 04表示条件とロール制限の設定
- 05UI 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:表示条件
Step 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 | 状態が「新規」である |
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である |
Step 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"
Step 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"
Step 5.4:UI アクションの表示に関するベストプラクティス
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:クライアントサイドとサーバーサイドのスクリプト
Step 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"
Step 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"
Step 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 アクションでの g_form の使用
Step 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');
Step 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:アクション後のリダイレクト
Step 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;
Step 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:アクションのセキュリティ
Step 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"
Step 9.2:UI アクション用の 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"
Step 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:一般的なユースケース
Step 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"
Step 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"
Step 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 アクションのテスト
Step 11.1:既存の UI アクションを検索する
テーブルのすべての UI アクションを検索する:
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 アクションを検索する:
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:テストチェックリスト
テスト前:
- UI アクションが有効になっている
- テーブルが正しい
- 表示場所が設定されている(form_button、list_banner_button など)
- テストシナリオで条件が true を返す
- スクリプトに構文エラーがない
機能テスト:
-
表示テスト:
- 条件が true のときにボタンが表示されますか?
- 条件が false のときにボタンが非表示になりますか?
- 正しいロールのユーザーにのみボタンが表示されますか?
-
クライアントスクリプトのテスト:
- onclick の検証は機能しますか?
- エラーメッセージは表示されますか?
- 確認ダイアログは表示されますか?
- フォームは正しく送信されますか?
-
サーバースクリプトのテスト:
- レコードは正しく更新されますか?
- リダイレクトは機能しますか?
- 作業メモは追加されますか?
- 監査証跡は作成されますか?
-
セキュリティテスト:
- 権限のないユーザーがアクションにアクセスできますか?
- ユーザーはクライアント側の検証を回避できますか?
- サーバーサイドでのロールチェックは設定されていますか?
Step 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 アクションのフィールドをローカルに取得する
スクリプトフィールドと最新状態のメタデータの両方を取得します:
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 アクションの作成 | SN-Create-Record | sys_ui_action テーブル |
| UI アクションの更新 | SN-Update-Record | script、condition を変更 |
| UI アクションのクエリ | SN-Query-Table | テーブル、名前で検索 |
| スキーマの取得 | SN-Get-Table-Schema | 使用可能なフィールドを確認 |
| ローカルスクリプトのサイクル | SN-Get-Record + SN-Update-Record | 取得、最新状態を確認、編集、プッシュ |
| テストスクリプトの実行 | SN-Execute-Background-Script | サーバーロジックをデバッグ |
ベストプラクティス
- わかりやすい名前: 明確で動作を表す名前を使用します(例:「管理者にエスカレーション」)。
- 順序の統一: 標準化された順序番号(100、200、300)を使用します。
- 必ず検証: クライアント側(ユーザー体験)とサーバー側(セキュリティ)の両方を検証します。
- 条件の使用: 無効なボタンを表示するのではなく、関係のないボタンを非表示にします。
- コメントの追加: 目的と依存関係を記録します。
- ロールベースのアクセス制御: 機密性の高いアクションは必ず制限します。
- 監査証跡: 重要なアクションにはすべて作業メモを追加します。
- エラー処理: サーバースクリプトを try-catch ブロックで囲みます。
- 十分なテスト: すべての条件、ロール、エッジケースをテストします。
- バージョン管理: バージョン管理のため、スクリプトをローカルに同期します。
トラブルシューティング
ボタンが表示されない
症状: UI アクションのボタンがフォーム/リストに表示されない 原因:
- UI アクションが無効になっている
- 条件の評価結果が 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- ローカルでのスクリプト開発