business-rules について
ServiceNow のサーバーサイド自動化を開発する開発者向けのスキルです。データ検証、レコード更新、ワークフロー起動に使うビジネスルールについて、実行タイミング、Glide API、性能、テスト、更新前のリモート状態確認を説明します。
主な機能
- 01ビジネスルールの実行タイミングと条件
- 02current と previous のレコード処理
- 03条件フィールドとスクリプト設定
- 04Glide API と性能の実践
- 05テスト、ログ、安全なスクリプト更新
活用シーン
- 01保存前のレコード検証
- 02変更後の関連レコード作成
- 03時間のかかる処理の非同期実行
- 04ビジネスルールの安全なテストと更新
MCP 対応
SN-Create-Record、SN-Query-Table、SN-Update-Record などの ServiceNow MCP ツールを使ったビジネスルールの作成、検索、テスト、更新方法を説明します。
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ happy-technologies-llc/ happy-platform-skills --skill business-rulesインストール後、Claude Code で/business-rulesを入力して読み込まれたか確認します。
詳細
ビジネスルール開発
概要
ビジネスルールは、レコードの表示、挿入、更新、削除、またはクエリの実行時に実行されるサーバーサイドスクリプトです。ServiceNow の自動化の基盤となります。
- どのような問題を解決しますか? レコード単位のロジックを自動化し、データの整合性を確保して、レコードの変更に基づいてワークフローをトリガーします
- このスキルは誰が使うべきですか? カスタム自動化ロジックを構築する ServiceNow 開発者
- 期待される成果: ServiceNow のベストプラクティスに従った、適切に構成され、パフォーマンスに優れたビジネスルール
前提条件
- ロール:
adminまたはスコープ付きアプリケーション開発者ロール - 知識: JavaScript の基礎、GlideRecord API の基本
- アクセス権: sys_script テーブル、ビジネスルールの対象テーブル
- 関連スキル:
admin/script-execution、admin/update-set-management
ビジネスルールを使用する場面
タイミングの一覧
| タイミング | トリガー | 用途 | current/previous |
|---|---|---|---|
| before | 挿入/更新/削除 | データの検証、フィールド値の設定、操作の中止 | どちらも利用可能 |
| after | 挿入/更新/削除 | 関連レコードの作成、通知の送信、外部連携 | どちらも利用可能 |
| async | 挿入/更新/削除 | 実行時間の長い操作、外部 API 呼び出し | current のみ |
| display | クエリ/表示 | 実行時の値の計算、scratchpad への値の設定 | current のみ |
判断ガイド
BEFORE を使用する場面:
- 他のフィールドに基づいてデフォルト値を設定する場合
- 保存前にデータを検証する場合
- コミット前にフィールド値を変更する場合
current.setAbortAction(true)で無効な操作を中止する場合
AFTER を使用する場面:
- 子レコードや関連レコードを作成する場合
- 通知を送信する場合(レコードのコミット後)
- 他のテーブルを更新する場合
- ワークフローをトリガーする場合
ASYNC を使用する場面:
- 外部 REST/SOAP 呼び出しを行う場合
- 大量のデータセットを処理する場合
- ユーザーをブロックせずに失敗してもよい操作を行う場合
- 実行時間の長い計算を行う場合
DISPLAY を使用する場面:
- フォーム表示専用の値を計算する場合
- クライアントスクリプト用に g_scratchpad に値を設定する場合
- 実行時のみのフィールド値(保存されない)を扱う場合
手順
フェーズ 1:ビジネスルールの作成
ステップ 1.1:テーブルスキーマの確認
まず、対象テーブルの構造を把握します。
MCP を使用:
Tool: SN-Get-Table-Schema
Parameters:
table_name: incident
ステップ 1.2:基本的なビジネスルールの作成
MCP を使用:
Tool: SN-Create-Record
Parameters:
table_name: sys_script
data:
name: Set Priority Based on Impact and Urgency
collection: incident
active: true
when: before
order: 100
filter_condition: impactCHANGES^ORurgencyCHANGES
script: |
(function executeRule(current, previous /*null when async*/) {
// Calculate priority from impact and urgency matrix
var impact = parseInt(current.impact);
var urgency = parseInt(current.urgency);
// Priority matrix: 1=Critical, 2=High, 3=Moderate, 4=Low, 5=Planning
var matrix = {
'1-1': 1, '1-2': 2, '1-3': 3,
'2-1': 2, '2-2': 3, '2-3': 4,
'3-1': 3, '3-2': 4, '3-3': 5
};
var key = impact + '-' + urgency;
var newPriority = matrix[key] || 4;
if (current.priority != newPriority) {
current.priority = newPriority;
gs.info('Priority calculated: ' + newPriority + ' for ' + current.number);
}
})(current, previous);
ステップ 1.3:After ビジネスルールの作成
MCP を使用:
Tool: SN-Create-Record
Parameters:
table_name: sys_script
data:
name: Create Related Task on P1 Incident
collection: incident
active: true
when: after
order: 200
filter_condition: priority=1^stateVALCHANGES1
script: |
(function executeRule(current, previous /*null when async*/) {
// Only on insert or when becoming P1
if (current.operation() == 'insert' ||
(previous && previous.priority != 1)) {
var task = new GlideRecord('sc_task');
task.initialize();
task.short_description = 'P1 Response: ' + current.short_description;
task.description = 'Critical incident requires immediate response.\n\nIncident: ' + current.number;
task.assignment_group = current.assignment_group;
task.assigned_to = current.assigned_to;
task.priority = 1;
task.parent = current.sys_id;
var taskId = task.insert();
gs.info('Created P1 response task: ' + taskId + ' for ' + current.number);
}
})(current, previous);
ステップ 1.4:Async ビジネスルールの作成
MCP を使用:
Tool: SN-Create-Record
Parameters:
table_name: sys_script
data:
name: Notify External System on Incident Create
collection: incident
active: true
when: async
order: 500
action_insert: true
script: |
(function executeRule(current, previous /*null when async*/) {
try {
var request = new sn_ws.RESTMessageV2('External Notification', 'POST');
request.setStringParameterNoEscape('incident_number', current.number.toString());
request.setStringParameterNoEscape('short_description', current.short_description.toString());
request.setStringParameterNoEscape('priority', current.priority.toString());
var response = request.execute();
var httpStatus = response.getStatusCode();
if (httpStatus == 200 || httpStatus == 201) {
gs.info('External notification sent for: ' + current.number);
} else {
gs.error('External notification failed: ' + httpStatus + ' - ' + response.getBody());
}
} catch (e) {
gs.error('External notification error: ' + e.message);
}
})(current, previous);
フェーズ 2:current オブジェクトと previous オブジェクトの使用
ステップ 2.1:current と previous の違い
current オブジェクトは、処理中のレコードを表します。previous オブジェクトには、現在のトランザクションより前のフィールド値が含まれます。
主な違い:
| 項目 | current | previous |
|---|---|---|
| 利用可能な範囲 | すべてのビジネスルール | before/after のみ(async では null) |
| 変更可能 | はい(before ルール) | いいえ(読み取り専用) |
| 挿入操作 | 値あり | null |
| 値 | 新規/変更後 | 変更前の元の値 |
ステップ 2.2:フィールドの変更の検出
.changes() メソッドを使用する方法(推奨):
Tool: SN-Create-Record
Parameters:
table_name: sys_script
data:
name: Log State Changes
collection: incident
active: true
when: after
order: 100
script: |
(function executeRule(current, previous /*null when async*/) {
// EFFICIENT: Exit early if field hasn't changed
if (!current.state.changes()) {
return; // No work to do
}
// Get old and new values
var oldState = previous ? previous.state.getDisplayValue() : '(new)';
var newState = current.state.getDisplayValue();
gs.info('Incident ' + current.number + ' state changed: ' + oldState + ' -> ' + newState);
// Add work note
current.work_notes = 'State changed from ' + oldState + ' to ' + newState;
})(current, previous);
手動で変更を検出する方法(.changes() が適さない場合):
// For complex comparisons or calculated changes
if (previous && current.priority != previous.priority) {
// Priority changed
}
// For reference fields, compare sys_id
if (previous && current.assigned_to.toString() != previous.assigned_to.toString()) {
// Assignment changed
}
// For multiple fields
var fieldsToCheck = ['state', 'priority', 'assigned_to'];
var changedFields = [];
for (var i = 0; i < fieldsToCheck.length; i++) {
var field = fieldsToCheck[i];
if (current[field].changes()) {
changedFields.push(field);
}
}
if (changedFields.length > 0) {
gs.info('Changed fields: ' + changedFields.join(', '));
}
ステップ 2.3:操作タイプの確認
// Determine what triggered the business rule
var operation = current.operation();
switch (operation) {
case 'insert':
// Record is being created
gs.info('New record: ' + current.getTableName());
break;
case 'update':
// Record is being updated
gs.info('Update to: ' + current.getUniqueValue());
break;
case 'delete':
// Record is being deleted
gs.info('Deleting: ' + current.getUniqueValue());
break;
}
フェーズ 3:条件フィールドとスクリプト条件
ステップ 3.1:条件フィールド(シンプルな条件に推奨)
条件フィールドではエンコードされたクエリを使用し、スクリプトの実行前に評価されます。次の理由から、より効率的です。
- データベースレベルで評価されます
- 条件を満たさない場合、スクリプトは実行されません
- JavaScript によるオーバーヘッドがありません
条件フィールドのベストプラクティス:
# Only run on active P1 incidents
active=true^priority=1
# Only when state changes to Resolved
stateVALCHANGES6
# Only when assigned_to changes
assigned_toCHANGES
# Multiple conditions (AND)
active=true^priority=1^stateVALCHANGES6
# Only on insert (no previous value for state)
stateISEMPTYfalse^ORstateISEMPTY
よく使われる条件演算子:
| 演算子 | 意味 | 例 |
|---|---|---|
| CHANGES | フィールド値が変更された | stateCHANGES |
| VALCHANGES | 特定の値に変更された | stateVALCHANGES6 |
| CHANGESFROM | 特定の値から変更された | stateCHANGESFROM1 |
| = | 等しい | priority=1 |
| != | 等しくない | state!=7 |
| ISEMPTY | フィールドが空 | assigned_toISEMPTY |
| ISNOTEMPTY | フィールドに値がある | assigned_toISNOTEMPTY |
ステップ 3.2:スクリプト条件(複雑なロジック向け)
条件フィールドでロジックを表現できない場合に限り、スクリプト条件を使用してください。
MCP の使用:
Tool: SN-Create-Record
Parameters:
table_name: sys_script
data:
name: Complex Condition Example
collection: incident
active: true
when: before
order: 100
condition: |
// Script condition - returns true/false
// Available: current, previous
// Check if escalating (priority going from higher number to lower)
if (current.priority.changes()) {
var oldPri = previous ? parseInt(previous.priority) : 5;
var newPri = parseInt(current.priority);
return newPri < oldPri; // True if escalating
}
return false;
script: |
(function executeRule(current, previous /*null when async*/) {
// This only runs if condition returned true
current.work_notes = 'Incident escalated from P' + previous.priority + ' to P' + current.priority;
gs.info('Escalation detected: ' + current.number);
})(current, previous);
スクリプト条件を使用するケース:
- 複雑な日付計算
- 複数のテーブルにまたがる検証
- ユーザーロールに基づく動的な条件
- 複数のフィールドを使用する計算
- GlideRecord クエリが必要な条件
フェーズ 4:一般的な Glide API メソッド
ステップ 4.1:GlideRecord の基本
// Query records
var gr = new GlideRecord('incident');
gr.addQuery('active', true);
gr.addQuery('priority', 1);
gr.orderByDesc('sys_created_on');
gr.setLimit(10);
gr.query();
while (gr.next()) {
gs.info('Found: ' + gr.number + ' - ' + gr.short_description);
}
// Get single record by sys_id
var incident = new GlideRecord('incident');
if (incident.get('sys_id_value')) {
gs.info('Found: ' + incident.number);
}
// Get by field value
var user = new GlideRecord('sys_user');
if (user.get('user_name', 'admin')) {
gs.info('Admin sys_id: ' + user.sys_id);
}
// Insert new record
var newTask = new GlideRecord('task');
newTask.initialize();
newTask.short_description = 'New task from business rule';
newTask.assignment_group = current.assignment_group;
var sysId = newTask.insert();
// Update record
var toUpdate = new GlideRecord('incident');
if (toUpdate.get('sys_id_value')) {
toUpdate.work_notes = 'Updated via business rule';
toUpdate.update();
}
// Delete record (use with caution)
var toDelete = new GlideRecord('task');
if (toDelete.get('sys_id_value')) {
toDelete.deleteRecord();
}
ステップ 4.2:GlideSystem(gs)メソッド
// Logging
gs.info('Information message');
gs.warn('Warning message');
gs.error('Error message');
gs.debug('Debug message'); // Requires debug enabled
// User context
var userId = gs.getUserID();
var userName = gs.getUserName();
var userDisplayName = gs.getUserDisplayName();
// Check roles
if (gs.hasRole('admin')) {
// Admin-only logic
}
if (gs.hasRole('itil') || gs.hasRole('catalog_admin')) {
// ITIL or catalog admin logic
}
// Date/Time
var now = gs.now(); // Current date/time string
var nowDT = gs.nowDateTime(); // GlideDateTime
var today = gs.beginningOfToday(); // Start of today
var daysAgo = gs.daysAgo(7); // 7 days ago
// Properties
var propValue = gs.getProperty('my.property.name', 'default');
gs.setProperty('my.property.name', 'new_value');
// Generate GUID
var guid = gs.generateGUID();
// Include script include
gs.include('MyScriptInclude');
var util = new MyScriptInclude();
// Nil check (empty string, null, undefined)
if (gs.nil(current.assigned_to)) {
// Field is empty
}
// Event queue (trigger events)
gs.eventQueue('incident.created', current, current.number, current.priority);
ステップ 4.3:GlideDateTime の操作
// Current time
var now = new GlideDateTime();
// Create from string
var dt = new GlideDateTime('2026-02-06 10:30:00');
// Add/subtract time
var future = new GlideDateTime();
future.addDays(5);
future.addHours(2);
future.addMinutes(30);
// Compare dates
var dt1 = new GlideDateTime();
var dt2 = new GlideDateTime(current.sys_created_on);
if (dt1.after(dt2)) {
gs.info('dt1 is after dt2');
}
// Calculate duration
var duration = GlideDateTime.subtract(dt2, dt1); // Returns GlideDuration
var seconds = duration.getNumericValue() / 1000;
var displayValue = duration.getDisplayValue(); // "2 Days 3 Hours"
// Business time calculations
var schedule = new GlideSchedule('sys_id_of_schedule');
var dur = schedule.duration(dt1, dt2);
// Format output
var formatted = now.getDisplayValue(); // User's timezone
var internal = now.getValue(); // Internal format (UTC)
var date = now.getLocalDate().getValue(); // Date only
ステップ 4.4:件数と合計に使う GlideAggregate
// Count records
var ga = new GlideAggregate('incident');
ga.addQuery('active', true);
ga.addAggregate('COUNT');
ga.query();
if (ga.next()) {
var count = ga.getAggregate('COUNT');
gs.info('Active incidents: ' + count);
}
// Count by group
var ga = new GlideAggregate('incident');
ga.addQuery('active', true);
ga.addAggregate('COUNT');
ga.groupBy('priority');
ga.orderByAggregate('COUNT', false); // Descending
ga.query();
while (ga.next()) {
var priority = ga.priority.getDisplayValue();
var count = ga.getAggregate('COUNT');
gs.info(priority + ': ' + count + ' incidents');
}
// Sum values
var ga = new GlideAggregate('sc_task');
ga.addQuery('active', true);
ga.addAggregate('SUM', 'time_worked');
ga.query();
if (ga.next()) {
var totalTime = ga.getAggregate('SUM', 'time_worked');
}
// Average
ga.addAggregate('AVG', 'priority');
// Min/Max
ga.addAggregate('MIN', 'sys_created_on');
ga.addAggregate('MAX', 'sys_created_on');
フェーズ 5:エラー処理とログ記録
ステップ 5.1:包括的なエラー処理パターン
MCP の使用:
Tool: SN-Create-Record
Parameters:
table_name: sys_script
data:
name: Robust Error Handling Example
collection: incident
active: true
when: after
order: 100
script: |
(function executeRule(current, previous /*null when async*/) {
var BR_NAME = 'Robust Error Handling Example';
try {
// Validate inputs
if (gs.nil(current.sys_id)) {
throw new Error('Current record has no sys_id');
}
// Main logic
var relatedCI = new GlideRecord('cmdb_ci');
if (!relatedCI.get(current.cmdb_ci)) {
gs.warn('[' + BR_NAME + '] No CI found for incident: ' + current.number);
return; // Exit gracefully
}
// Perform operations
relatedCI.operational_status = 2; // Under maintenance
relatedCI.update();
gs.info('[' + BR_NAME + '] Updated CI: ' + relatedCI.name);
} catch (e) {
// Log error with context
gs.error('[' + BR_NAME + '] Error: ' + e.message);
gs.error('[' + BR_NAME + '] Incident: ' + current.number);
gs.error('[' + BR_NAME + '] Stack: ' + e.stack);
// Optionally create incident for business rule error
// createErrorIncident(BR_NAME, e, current);
}
})(current, previous);
ステップ 5.2:ユーザーへのフィードバックを伴う処理の中止
// Before business rule - prevent invalid save
(function executeRule(current, previous) {
var errors = [];
// Validation 1: Required field
if (current.priority == 1 && gs.nil(current.assigned_to)) {
errors.push('P1 incidents must have an assigned user');
}
// Validation 2: Business logic
if (current.state == 6 && gs.nil(current.resolution_notes)) {
errors.push('Resolution notes are required when resolving');
}
// Validation 3: Cross-field validation
if (current.impact == 1 && current.urgency == 1 && current.priority != 1) {
errors.push('Impact 1 + Urgency 1 must equal Priority 1');
}
// Abort if errors
if (errors.length > 0) {
var errorMsg = errors.join('\n');
gs.addErrorMessage(errorMsg);
current.setAbortAction(true);
gs.info('[Validation BR] Aborted save: ' + errorMsg);
}
})(current, previous);
フェーズ 6:パフォーマンスの最適化
ステップ 6.1:効率化のために .changes() を使用する
誤り — スクリプト全体が常に実行されます:
// Inefficient - script runs on every update
(function executeRule(current, previous) {
if (previous && current.state != previous.state) {
// State changed - do work
}
})(current, previous);
正しい方法 — 条件フィールドと .changes() を使用します:
Filter Condition: stateCHANGES
Script:
(function executeRule(current, previous) {
// Script only runs when state changes
// Additional validation with .changes() for safety
if (current.state.changes()) {
// Do work
}
})(current, previous);
ステップ 6.2:GlideRecord クエリを最小限にする
誤り — ループ内でクエリを実行します:
var incidents = new GlideRecord('incident');
incidents.query();
while (incidents.next()) {
// BAD: Query for each incident
var user = new GlideRecord('sys_user');
user.get(incidents.assigned_to);
// ...
}
正しい方法 — ドットウォーキングまたはバッチクエリを使用します:
// Option 1: Dot-walking (single query)
var incidents = new GlideRecord('incident');
incidents.query();
while (incidents.next()) {
var userName = incidents.assigned_to.name; // Dot-walk
var userEmail = incidents.assigned_to.email;
}
// Option 2: Batch query with lookup map
var userIds = [];
var incidents = new GlideRecord('incident');
incidents.query();
while (incidents.next()) {
if (!gs.nil(incidents.assigned_to)) {
userIds.push(incidents.assigned_to.toString());
}
}
var userMap = {};
if (userIds.length > 0) {
var users = new GlideRecord('sys_user');
users.addQuery('sys_id', 'IN', userIds.join(','));
users.query();
while (users.next()) {
userMap[users.sys_id.toString()] = {
name: users.name.toString(),
email: users.email.toString()
};
}
}
ステップ 6.3:Order と Active フラグのベストプラクティス
| プラクティス | 推奨事項 |
|---|---|
| Order | 検証には 100-199、フィールド設定には 200-399、外部処理/非同期処理には 400+ を使用します |
| Active | 開発中は false に設定し、テスト後に有効にします |
| Condition | スクリプト条件ではなく、フィルター条件フィールドを使用します |
| Inheritance | 「Inherits」は慎重に設定します。通常はチェックを外したままにします |
ステップ 6.4:再帰的な更新を避ける
危険 — 無限ループを引き起こす可能性があります:
// After business rule on incident
(function executeRule(current, previous) {
current.work_notes = 'Updated at ' + gs.now();
current.update(); // DANGER: Triggers business rules again!
})(current, previous);
安全 — workflow を false に設定するか、before ルールを使用します:
// Option 1: Disable workflow on update
var gr = new GlideRecord('incident');
if (gr.get(current.sys_id)) {
gr.setWorkflow(false); // Skip business rules
gr.autoSysFields(false); // Skip sys field updates
gr.work_notes = 'Updated at ' + gs.now();
gr.update();
}
// Option 2: Use before rule instead (preferred)
// Before business rule - modifies current directly
(function executeRule(current, previous) {
current.work_notes = 'Updated at ' + gs.now();
// No .update() needed - current is saved automatically
})(current, previous);
フェーズ 7:ビジネスルールのテスト
ステップ 7.1:既存のビジネスルールをクエリする
MCP の使用:
Tool: SN-Query-Table
Parameters:
table_name: sys_script
query: collection=incident^active=true
fields: name,when,order,filter_condition,active
limit: 50
ステップ 7.2:バックグラウンドスクリプトでテストする
MCP の使用:
Tool: SN-Execute-Background-Script
Parameters:
script: |
// Test business rule by simulating record operation
var testIncident = new GlideRecord('incident');
testIncident.initialize();
testIncident.short_description = 'Test incident for BR testing';
testIncident.caller_id = gs.getUserID();
testIncident.impact = 1;
testIncident.urgency = 1;
// Insert will trigger before/after insert rules
var sysId = testIncident.insert();
gs.info('Created test incident: ' + sysId);
// Verify priority was calculated
testIncident.get(sysId);
gs.info('Priority after BR: ' + testIncident.priority.getDisplayValue());
// Clean up (optional)
// testIncident.deleteRecord();
description: Test priority calculation business rule
ステップ 7.3:システムログを確認する
MCP の使用:
Tool: SN-Query-Table
Parameters:
table_name: syslog
query: messageLIKEbusiness rule^ORmessageLIKE[BR]^sys_created_on>javascript:gs.minutesAgo(10)
fields: message,level,sys_created_on,source
limit: 50
ステップ 7.4:明示的なローカル開発サイクル
現在のスクリプトと鮮度メタデータを取得します:
Tool: SN-Get-Record
Parameters:
table_name: sys_script
sys_id: [business_rule_sys_id]
fields: sys_id,script,sys_updated_on,sys_mod_count
instance: dev
script フィールドをローカルに保存し、IDE で編集します。プッシュする直前に
SN-Get-Record を再度実行し、sys_updated_on、sys_mod_count、
元のスクリプトを比較します。リモートレコードが変更されている場合は、
上書きせずにローカルでマージします。変更がない場合は、レビュー済みのスクリプトフィールドのみをプッシュします:
Tool: SN-Update-Record
Parameters:
table_name: sys_script
sys_id: [business_rule_sys_id]
data:
script: [reviewed_local_script]
instance: dev
ツールの使用概要
| 操作 | MCP ツール | 目的 |
|---|---|---|
| BR の作成 | SN-Create-Record (sys_script) | 新しいビジネスルールを作成する |
| BR の更新 | SN-Update-Record | 既存のビジネスルールを変更する |
| BR のクエリ | SN-Query-Table | テーブル上のビジネスルールを検索する |
| BR のテスト | SN-Execute-Background-Script | レコード操作をシミュレートする |
| デバッグ | SN-Query-Table (syslog) | 実行ログを確認する |
| ローカル開発 | SN-Get-Record + SN-Update-Record | 明示的な取得、鮮度確認、編集、プッシュ |
| スキーマの取得 | SN-Get-Table-Schema | テーブル構造を把握する |
ベストプラクティス
コード品質
- IIFE パターンを使用する: すべてのスクリプトを
(function executeRule(current, previous) {...})(current, previous);で囲みます - 分かりやすい変数名を付ける:
gr2ではなくexistingTaskのような説明的な名前を使用します - 複雑なロジックにはコメントを付ける: 何をするかではなく、なぜそうするのかを説明します
- マジックナンバーを避ける:
state=6のような値には、定数またはコメントを使用します
パフォーマンス
- .changes() メソッドを使用する: フィールドに変更がなければ、必ず早期に終了します
- Condition フィールドを優先する: データベースレベルのフィルタリングは、スクリプトよりも高速です
- クエリを制限する: setLimit() を使用し、ループ内でのクエリを避けます
- 順序を慎重に設定する: 数値が小さいほど先に実行されます。検証は早い順序(100-199)に設定します
信頼性
- 必ずエラーを処理する: 特に外部呼び出しは try-catch で囲みます
- 意味のあるメッセージをログに記録する: レコード識別子とフィールド値を含めます
- 複数のロールでテストする: ACL によってビジネスルールが妨げられないことを確認します
- 再帰を避ける: 同じテーブルを更新する場合は setWorkflow(false) を使用します
保守性
- 1つのルールにつき目的は1つにする: 複雑なルールは、目的に絞ったコンポーネントに分割します
- Script Includes を使用する: 再利用可能なロジックを script includes に移します
- 依存関係を文書化する: 連携する他のルールやスクリプトを記録します
- バージョン管理:
SN-Get-Recordで取得し、ベースリビジョンを Git に保持して鮮度を確認した後、SN-Update-Recordでプッシュします
トラブルシューティング
ビジネスルールが実行されない
症状: レコードは保存されますが、ビジネスルールのスクリプトが実行されません 原因:
- Active = false
- フィルター条件に一致していない
- abort アクションとの順序の競合
- ACL によってフィールドへのアクセスが妨げられている 解決策:
Tool: SN-Query-Table
Parameters:
table_name: sys_script
query: collection=incident^name=*priority*
fields: name,active,when,filter_condition,order
無限ループ/スタックオーバーフロー
症状: エラー「Maximum call stack size exceeded」またはタイムアウト 原因: ビジネスルールが同じテーブルに対して .update() を呼び出し、自身を再度実行している 解決策:
// Use setWorkflow(false) to prevent recursion
var gr = new GlideRecord('incident');
gr.get(current.sys_id);
gr.setWorkflow(false);
gr.update();
Previous が null
症状: previous.fieldname へのアクセス時にエラーが発生する 原因: insert 操作または非同期ルールでは previous が null になる 解決策:
// Always check for null
if (previous && current.state != previous.state) {
// Safe to access previous
}
// Or use operation check
if (current.operation() != 'insert' && previous) {
// previous is available
}
変更が検出されない
症状: フィールドが変更されても .changes() が false を返す 原因: フィールドが同じ値に設定されている、またはクエリ後に変更が行われている 解決策:
// Log actual values for debugging
gs.info('Current state: ' + current.state + ', Previous: ' + (previous ? previous.state : 'null'));
gs.info('Changes: ' + current.state.changes());
// Use condition field for reliable change detection
// Filter: stateCHANGES
例
例 1:カテゴリに基づく自動割り当て
(function executeRule(current, previous) {
// Exit early if category hasn't changed
if (!current.category.changes()) {
return;
}
// Assignment mapping
var categoryAssignment = {
'network': 'Network Team',
'hardware': 'Desktop Support',
'software': 'Application Support',
'database': 'DBA Team'
};
var category = current.category.toString();
var groupName = categoryAssignment[category];
if (groupName) {
var group = new GlideRecord('sys_user_group');
if (group.get('name', groupName)) {
current.assignment_group = group.sys_id;
gs.info('Auto-assigned to ' + groupName + ' based on category');
}
}
})(current, previous);
例 2:SLA エスカレーションの警告
(function executeRule(current, previous) {
// Check SLA breach approaching (80% of response time)
var sla = new GlideRecord('task_sla');
sla.addQuery('task', current.sys_id);
sla.addQuery('stage', 'in_progress');
sla.query();
while (sla.next()) {
var percentComplete = sla.percentage.floatValue();
if (percentComplete >= 80 && percentComplete < 100) {
// Approaching breach - escalate
if (current.priority > 1) {
current.priority = parseInt(current.priority) - 1;
current.work_notes = 'Auto-escalated: SLA at ' + Math.round(percentComplete) + '%';
gs.info('SLA escalation for ' + current.number + ' - ' + percentComplete + '%');
}
}
}
})(current, previous);
例 3:機密フィールドの変更に対する監査証跡
(function executeRule(current, previous) {
var sensitiveFields = ['priority', 'assignment_group', 'assigned_to', 'state'];
var changes = [];
for (var i = 0; i < sensitiveFields.length; i++) {
var field = sensitiveFields[i];
if (current[field].changes()) {
changes.push({
field: field,
from: previous ? previous[field].getDisplayValue() : '(empty)',
to: current[field].getDisplayValue()
});
}
}
if (changes.length > 0) {
var auditNote = 'Field Changes:\n';
for (var j = 0; j < changes.length; j++) {
var c = changes[j];
auditNote += '- ' + c.field + ': "' + c.from + '" -> "' + c.to + '"\n';
}
auditNote += '\nChanged by: ' + gs.getUserDisplayName();
current.work_notes = auditNote;
gs.info('Audit trail recorded for ' + current.number);
}
})(current, previous);
関連スキル
admin/script-execution- バックグラウンドスクリプトのパターンと実行admin/update-set-management- ビジネスルールの変更を追跡development/script-includes- 再利用可能なサーバーサイドライブラリcatalog/ui-policies- クライアントサイドのフィールド制御(BR を補完)security/acl-management- BR の実行に影響するアクセス制御