scheduled-jobs について
ServiceNow の開発者と管理者向けに、スケジュールされたバックグラウンドジョブの作成と管理を扱うスキルです。実行条件や性能、エラー処理、監視について説明します。
主な機能
- 01定期実行・単発ジョブの作成
- 02実行頻度と条件の設定
- 03フィルターとバッチによる処理の最適化
- 04エラー処理とイベント通知
- 05ログの検索とジョブの監視
活用シーン
- 01定期的なレコード整理や処理
- 02レポートやデータ同期のスケジュール実行
- 03未実行・停止したバックグラウンドジョブの調査
MCP 対応
MCP ツールによる ServiceNow のスケジュールジョブとログの作成・検索、バックグラウンドスクリプトの実行例を紹介します。
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ happy-technologies-llc/ happy-platform-skills --skill scheduled-jobsインストール後、Claude Code で/scheduled-jobsを入力して読み込まれたか確認します。
詳細
スケジュールジョブの開発
概要
このスキルでは、自動バックグラウンド処理のための ServiceNow スケジュールジョブの作成と管理について説明します。
- スケジュールスクリプト実行(
sysauto_script) - cron のようなスケジュールで繰り返し実行されるジョブ - システムトリガー(
sys_trigger) - 1 回限り、または条件に応じて実行されるジョブ - 実行頻度のパターン - 日次、週次、月次、cron 式
- 条件付き実行 - 特定の条件が満たされた場合にのみ実行
- パフォーマンスの最適化 - 処理が必要なレコードに絞り込む(重要!)
- エラー処理 - 堅牢なエラー管理と通知
- 監視とデバッグ - 実行履歴を追跡し、問題をトラブルシューティング
使用するタイミング: 定期タスク、バッチ処理、データクリーンアップ、通知など、スケジュールに従って実行されるバックグラウンド操作を自動化する必要がある場合。
対象ユーザー: ユーザーの介入なしで ServiceNow の操作を自動化する必要がある開発者と管理者。
前提条件
- ロール:
admin(スケジュールジョブの作成と管理に必須) - アクセス権:
sysauto_script、sys_trigger、syslogテーブル - 知識: GlideRecord API、ServiceNow サーバーサイド JavaScript
- 関連スキル:
admin/script-execution- バックグラウンドスクリプトのパターンadmin/batch-operations- レコードの一括操作
テーブルアーキテクチャ
コアテーブル
| テーブル | 用途 | 主なフィールド |
|---|---|---|
sysauto_script | スケジュールスクリプト実行(繰り返し) | name、script、run_type、run_time、conditional |
sys_trigger | システムトリガー(1 回限り/システムイベント) | name、script、next_action、trigger_type、state |
syslog | 実行ログ | message、level、source、sys_created_on |
sys_script | スクリプトインクルード(再利用可能なロジック) | name、script、active、api_name |
sysauto_script の実行タイプ
| 実行タイプ | 値 | 説明 |
|---|---|---|
| 1 回実行 | on_demand | 手動実行のみ |
| 日次 | daily | 指定した時刻に毎日実行 |
| 週次 | weekly | 指定した曜日に実行 |
| 月次 | monthly | 指定した日に実行 |
| 定期的 | periodically | 固定間隔で実行 |
| オンデマンド | on_demand | 手動トリガーのみ |
手順
フェーズ 1:スケジュールスクリプト実行の作成
ステップ 1.1:基本的な日次ジョブ
毎日実行されるスケジュールジョブを作成します。
MCP を使用:
Tool: SN-Create-Record
Parameters:
table_name: sysauto_script
data:
name: "Daily Incident Cleanup"
script: |
// Daily cleanup of resolved incidents older than 90 days
var cutoffDate = gs.daysAgo(90);
var gr = new GlideRecord('incident');
gr.addQuery('state', 'IN', '6,7,8'); // Resolved, Closed, Cancelled
gr.addQuery('sys_updated_on', '<', cutoffDate);
gr.setLimit(1000); // Process in batches
gr.query();
var archived = 0;
while (gr.next()) {
// Archive logic here
archived++;
}
gs.info('[Daily Incident Cleanup] Processed ' + archived + ' incidents');
active: true
run_type: daily
run_time: "02:00:00"
run_dayofweek: 1
主なフィールドの説明:
run_type: daily- 毎日実行run_time: "02:00:00"- 午前 2 時(24 時間形式を使用)run_dayofweek: 1- 月曜日(1=月、2=火、…、7=日)- 週次実行で使用
ステップ 1.2:週次ジョブ
毎週月曜日の午前 6 時に実行されるジョブを作成します。
MCP を使用:
Tool: SN-Create-Record
Parameters:
table_name: sysauto_script
data:
name: "Weekly SLA Report"
script: |
// Generate weekly SLA compliance report
gs.info('[Weekly SLA Report] Starting report generation');
var report = {
totalIncidents: 0,
slaBreaches: 0,
slaCompliance: 0
};
var gr = new GlideRecord('task_sla');
gr.addQuery('stage', 'IN', 'completed,cancelled');
gr.addQuery('sys_created_on', '>=', gs.daysAgo(7));
gr.query();
while (gr.next()) {
report.totalIncidents++;
if (gr.has_breached) {
report.slaBreaches++;
}
}
report.slaCompliance = report.totalIncidents > 0
? Math.round((1 - report.slaBreaches / report.totalIncidents) * 100)
: 100;
gs.info('[Weekly SLA Report] Results: ' + JSON.stringify(report));
active: true
run_type: weekly
run_time: "06:00:00"
run_dayofweek: 1
ステップ 1.3:月次ジョブ
毎月 1 日に実行されるジョブを作成します。
MCP を使用:
Tool: SN-Create-Record
Parameters:
table_name: sysauto_script
data:
name: "Monthly User Audit"
script: |
// Monthly audit of inactive users
gs.info('[Monthly User Audit] Starting audit');
var cutoffDate = gs.daysAgo(90);
var inactiveUsers = [];
var gr = new GlideRecord('sys_user');
gr.addQuery('active', true);
gr.addQuery('last_login_time', '<', cutoffDate);
gr.addQuery('last_login_time', '!=', '');
gr.query();
while (gr.next()) {
inactiveUsers.push({
user_id: gr.user_name.toString(),
name: gr.name.toString(),
last_login: gr.last_login_time.toString()
});
}
gs.info('[Monthly User Audit] Found ' + inactiveUsers.length + ' inactive users');
// Optional: Send report email
if (inactiveUsers.length > 0) {
var email = new GlideEmailOutbound();
email.setSubject('Monthly Inactive User Report');
email.setFrom('[email protected]');
email.addAddress('[email protected]');
email.setBody('Found ' + inactiveUsers.length + ' users inactive for 90+ days.\n\n' +
JSON.stringify(inactiveUsers, null, 2));
email.send();
}
active: true
run_type: monthly
run_time: "03:00:00"
run_dayofmonth: 1
ステップ 1.4:定期ジョブ(N 分/時間ごと)
15 分ごとに実行されるジョブを作成します。
MCP を使用:
Tool: SN-Create-Record
Parameters:
table_name: sysauto_script
data:
name: "Integration Queue Processor"
script: |
// Process integration queue every 15 minutes
gs.info('[Queue Processor] Starting processing');
var gr = new GlideRecord('x_custom_integration_queue');
gr.addQuery('state', 'pending');
gr.addQuery('retry_count', '<', 3);
gr.orderBy('priority');
gr.orderBy('sys_created_on');
gr.setLimit(100); // Batch size
gr.query();
var processed = 0;
var failed = 0;
while (gr.next()) {
try {
// Process queue item
processQueueItem(gr);
gr.state = 'completed';
gr.update();
processed++;
} catch (e) {
gr.retry_count = gr.retry_count + 1;
gr.error_message = e.message;
gr.state = gr.retry_count >= 3 ? 'failed' : 'pending';
gr.update();
failed++;
gs.error('[Queue Processor] Error: ' + e.message);
}
}
gs.info('[Queue Processor] Completed. Processed=' + processed + ', Failed=' + failed);
function processQueueItem(record) {
// Your processing logic here
}
active: true
run_type: periodically
run_period: 900000
注: run_period の単位はミリ秒です(900000 = 15 分)
| 間隔 | ミリ秒 |
|---|---|
| 1 分 | 60000 |
| 5 分 | 300000 |
| 15 分 | 900000 |
| 30 分 | 1800000 |
| 1 時間 | 3600000 |
| 4 時間 | 14400000 |
| 12 時間 | 43200000 |
フェーズ 2:条件付き実行
ステップ 2.1:条件付きジョブ(条件を満たした場合にのみ実行)
処理対象のレコードがある場合にのみ実行されるジョブを作成します。
MCP を使用:
Tool: SN-Create-Record
Parameters:
table_name: sysauto_script
data:
name: "Process Pending Approvals"
script: |
// Process stale approval requests
gs.info('[Approval Processor] Starting');
var staleThreshold = gs.hoursAgo(24);
var gr = new GlideRecord('sysapproval_approver');
gr.addQuery('state', 'requested');
gr.addQuery('sys_created_on', '<', staleThreshold);
gr.query();
var escalated = 0;
while (gr.next()) {
// Escalate stale approvals
escalateApproval(gr);
escalated++;
}
gs.info('[Approval Processor] Escalated ' + escalated + ' approvals');
function escalateApproval(approval) {
// Send reminder notification
var email = new GlideEmailOutbound();
email.setSubject('Reminder: Approval Pending - ' + approval.document_id.getDisplayValue());
email.addAddress(approval.approver.email);
email.setBody('You have a pending approval request that requires your attention.\n\n' +
'Document: ' + approval.document_id.getDisplayValue() + '\n' +
'Requested: ' + approval.sys_created_on.getDisplayValue());
email.send();
}
active: true
run_type: daily
run_time: "09:00:00"
conditional: true
condition: |
// Only run if there are stale approvals
var staleThreshold = gs.hoursAgo(24);
var gr = new GlideRecord('sysapproval_approver');
gr.addQuery('state', 'requested');
gr.addQuery('sys_created_on', '<', staleThreshold);
gr.setLimit(1);
gr.query();
answer = gr.hasNext();
主なフィールド:
conditional: true- 条件チェックを有効化condition-answer = true/falseを設定するスクリプト
ステップ 2.2:営業時間内のみ
営業時間内にのみ実行されるジョブを作成します。
MCP を使用:
Tool: SN-Create-Record
Parameters:
table_name: sysauto_script
data:
name: "Business Hours Queue Monitor"
script: |
// Monitor queue during business hours
gs.info('[Queue Monitor] Checking queue status');
var gr = new GlideRecord('x_custom_queue');
gr.addQuery('state', 'pending');
gr.addQuery('priority', 1);
gr.query();
if (gr.getRowCount() > 10) {
// Alert on high queue depth
gs.eventQueue('queue.high_depth', gr, gr.getRowCount());
}
active: true
run_type: periodically
run_period: 600000
conditional: true
condition: |
// Only run between 8 AM and 6 PM, Monday-Friday
var now = new GlideDateTime();
var hour = parseInt(now.getLocalTime().getHourOfDayLocalTime());
var dayOfWeek = now.getDayOfWeekLocalTime();
// Monday=2, Friday=6 in GlideDateTime
var isWeekday = dayOfWeek >= 2 && dayOfWeek <= 6;
var isBusinessHours = hour >= 8 && hour < 18;
answer = isWeekday && isBusinessHours;
フェーズ 3:パフォーマンスの最適化
重要: パフォーマンスの問題を避けるため、スケジュールジョブを最適化する必要があります。最もよくある間違いは、処理が必要なレコードだけに絞り込まず、すべてのレコードを処理することです。
ステップ 3.1:効率的なクエリパターン
悪いパターン(テーブル全体のスキャン):
// DON'T DO THIS - Scans entire table every time
var gr = new GlideRecord('incident');
gr.query();
while (gr.next()) {
if (gr.state == 1 && gr.priority == 1) {
// Process
}
}
良いパターン(絞り込んだクエリ):
// DO THIS - Only retrieves records needing processing
var gr = new GlideRecord('incident');
gr.addQuery('state', 1); // New
gr.addQuery('priority', 1); // P1
gr.addQuery('processed', false); // Not yet processed
gr.setLimit(100); // Batch limit
gr.query();
while (gr.next()) {
// Process
}
Step 3.2:バッチ処理パターン
MCP の使用:
Tool: SN-Create-Record
Parameters:
table_name: sysauto_script
data:
name: "Batch Data Processor"
script: |
// Efficient batch processing pattern
var BATCH_SIZE = 100;
var MAX_BATCHES = 10;
var TABLE = 'incident';
var QUERY = 'state=1^priority=1^u_needs_processing=true';
gs.info('[Batch Processor] Starting');
var totalProcessed = 0;
var batchNum = 0;
while (batchNum < MAX_BATCHES) {
var gr = new GlideRecord(TABLE);
gr.addEncodedQuery(QUERY);
gr.setLimit(BATCH_SIZE);
gr.query();
if (!gr.hasNext()) {
gs.info('[Batch Processor] No more records to process');
break;
}
var batchProcessed = 0;
while (gr.next()) {
try {
processRecord(gr);
gr.u_needs_processing = false;
gr.update();
batchProcessed++;
totalProcessed++;
} catch (e) {
gs.error('[Batch Processor] Error processing ' + gr.number + ': ' + e.message);
}
}
gs.info('[Batch Processor] Batch ' + (batchNum + 1) + ' complete: ' + batchProcessed + ' records');
batchNum++;
}
gs.info('[Batch Processor] Total processed: ' + totalProcessed);
function processRecord(record) {
// Your processing logic
}
active: true
run_type: periodically
run_period: 300000
Step 3.3:集計機能を使って件数を数える
悪いパターン(すべてのレコードを読み込みます):
var gr = new GlideRecord('incident');
gr.addQuery('active', true);
gr.query();
var count = gr.getRowCount(); // Loads all records first!
良いパターン(集計機能を使用します):
var ga = new GlideAggregate('incident');
ga.addQuery('active', true);
ga.addAggregate('COUNT');
ga.query();
var count = 0;
if (ga.next()) {
count = parseInt(ga.getAggregate('COUNT'));
}
Step 3.4:インデックスを考慮したクエリ
クエリではインデックス付きフィールドを使用してください。一般的なインデックス付きフィールドは次のとおりです。
| テーブル | インデックス付きフィールド |
|---|---|
incident | number, sys_id, active, state, priority, assigned_to, assignment_group |
task | number, sys_id, active, state, assigned_to, assignment_group |
sys_user | sys_id, user_name, email, active |
sys_user_group | sys_id, name, active |
インデックスの確認:
Tool: SN-Query-Table
Parameters:
table_name: sys_index
query: table=incident
fields: name,columns,active
フェーズ 4:エラー処理と通知
Step 4.1:包括的なエラー処理
MCP の使用:
Tool: SN-Create-Record
Parameters:
table_name: sysauto_script
data:
name: "Robust Data Sync"
script: |
// Robust scheduled job with comprehensive error handling
var JOB_NAME = 'Data Sync';
var startTime = new GlideDateTime();
var stats = {
processed: 0,
errors: 0,
skipped: 0,
errorDetails: []
};
gs.info('[' + JOB_NAME + '] Starting execution');
try {
// Main processing logic
var gr = new GlideRecord('x_custom_sync_queue');
gr.addQuery('state', 'pending');
gr.setLimit(100);
gr.query();
while (gr.next()) {
try {
if (!validateRecord(gr)) {
stats.skipped++;
continue;
}
syncRecord(gr);
gr.state = 'completed';
gr.update();
stats.processed++;
} catch (recordError) {
stats.errors++;
stats.errorDetails.push({
sys_id: gr.sys_id.toString(),
error: recordError.message
});
gr.state = 'error';
gr.error_message = recordError.message;
gr.update();
gs.error('[' + JOB_NAME + '] Record error: ' + gr.sys_id + ' - ' + recordError.message);
}
}
// Success summary
gs.info('[' + JOB_NAME + '] Summary: Processed=' + stats.processed +
', Errors=' + stats.errors + ', Skipped=' + stats.skipped);
} catch (fatalError) {
// Fatal error - entire job failed
gs.error('[' + JOB_NAME + '] FATAL ERROR: ' + fatalError.message);
gs.error('[' + JOB_NAME + '] Stack: ' + fatalError.stack);
// Send alert notification
sendAlertEmail(JOB_NAME, fatalError, stats);
} finally {
// Always log duration
var endTime = new GlideDateTime();
var duration = GlideDateTime.subtract(startTime, endTime).getNumericValue() / 1000;
gs.info('[' + JOB_NAME + '] Duration: ' + duration + ' seconds');
// Send error report if there were errors
if (stats.errors > 0) {
sendErrorReport(JOB_NAME, stats);
}
}
function validateRecord(record) {
return record.getValue('source_id') && record.getValue('target_table');
}
function syncRecord(record) {
// Your sync logic here
}
function sendAlertEmail(jobName, error, stats) {
var email = new GlideEmailOutbound();
email.setSubject('[ALERT] Scheduled Job Failed: ' + jobName);
email.setFrom('[email protected]');
email.addAddress('[email protected]');
email.setBody('The scheduled job "' + jobName + '" has failed.\n\n' +
'Error: ' + error.message + '\n\n' +
'Stats at failure:\n' + JSON.stringify(stats, null, 2));
email.send();
}
function sendErrorReport(jobName, stats) {
var email = new GlideEmailOutbound();
email.setSubject('[WARN] Scheduled Job Errors: ' + jobName);
email.setFrom('[email protected]');
email.addAddress('[email protected]');
email.setBody('The scheduled job "' + jobName + '" completed with errors.\n\n' +
'Summary:\n' +
'- Processed: ' + stats.processed + '\n' +
'- Errors: ' + stats.errors + '\n' +
'- Skipped: ' + stats.skipped + '\n\n' +
'Error Details:\n' + JSON.stringify(stats.errorDetails, null, 2));
email.send();
}
active: true
run_type: periodically
run_period: 900000
Step 4.2:イベントベースの通知
直接メールを送信する代わりに、通知には ServiceNow イベントを使用してください。
イベント登録の作成:
Tool: SN-Create-Record
Parameters:
table_name: sysevent_register
data:
event_name: "scheduled.job.failed"
suffix: "alert"
script_action: true
description: "Scheduled job failure alert"
スクリプトでの使用:
// In your scheduled job script
try {
// Processing logic
} catch (e) {
gs.eventQueue('scheduled.job.failed', null, JOB_NAME, e.message);
}
フェーズ 5:監視とデバッグ
Step 5.1:ジョブ実行履歴のクエリ
最近の実行を検索:
Tool: SN-Query-Table
Parameters:
table_name: syslog
query: source=Scheduled Job^sys_created_on>javascript:gs.hoursAgo(24)
fields: message,level,sys_created_on
limit: 100
orderByDesc: sys_created_on
特定のジョブログを検索:
Tool: SN-Query-Table
Parameters:
table_name: syslog
query: messageLIKE[Daily Incident Cleanup]^sys_created_on>javascript:gs.hoursAgo(24)
fields: message,level,sys_created_on
limit: 50
エラーログを検索:
Tool: SN-Query-Table
Parameters:
table_name: syslog
query: level=2^source=Scheduled Job^sys_created_on>javascript:gs.hoursAgo(24)
fields: message,sys_created_on,source
limit: 50
Step 5.2:ジョブのステータスを確認
すべてのアクティブなスケジュール済みジョブを一覧表示:
Tool: SN-Query-Table
Parameters:
table_name: sysauto_script
query: active=true
fields: name,run_type,run_time,run_period,next_action,sys_updated_on
limit: 100
特定のジョブを確認:
Tool: SN-Query-Table
Parameters:
table_name: sysauto_script
query: name=Daily Incident Cleanup
fields: name,active,run_type,run_time,next_action,script
Step 5.3:実行中のジョブをデバッグ
システムトリガーを確認:
Tool: SN-Query-Table
Parameters:
table_name: sys_trigger
query: next_action>javascript:gs.nowDateTime()^state=0
fields: name,next_action,trigger_type,state
limit: 50
停止したままのジョブを確認:
Tool: SN-Query-Table
Parameters:
table_name: sys_trigger
query: state=1^sys_updated_on<javascript:gs.hoursAgo(1)
fields: name,state,started_at,sys_updated_on
フェーズ 6:オンデマンド実行
Step 6.1:ジョブを直ちに実行
SN-Execute-Background-Script の使用:
Tool: SN-Execute-Background-Script
Parameters:
script: |
// Execute scheduled job on demand
var job = new GlideRecord('sysauto_script');
job.addQuery('name', 'Daily Incident Cleanup');
job.query();
if (job.next()) {
var sched = new ScheduleOnce();
sched.script = job.script;
sched.setName('Manual Run: ' + job.name);
sched.schedule();
gs.info('Job scheduled for immediate execution: ' + job.name);
}
description: Trigger scheduled job manually
Step 6.2:1 回限りのトリガーを作成
MCP の使用:
Tool: SN-Create-Record
Parameters:
table_name: sys_trigger
data:
name: "One-Time Data Migration"
script: |
gs.info('[Migration] Starting one-time migration');
// Your migration logic here
gs.info('[Migration] Complete');
next_action: 2026-02-06 14:00:00
trigger_type: 0
state: 0
トリガータイプの値:
| 値 | 説明 |
|---|---|
| 0 | 1 回実行 |
| 1 | 定期的に実行 |
| 2 | イベント発生時 |
フェーズ 7:高度なパターン
Step 7.1:自己スケジューリングパターン
非常に長時間かかる処理では、複数の実行を連鎖させます。
MCP の使用:
Tool: SN-Create-Record
Parameters:
table_name: sysauto_script
data:
name: "Large Data Migration"
script: |
// Self-scheduling pattern for large operations
var RECORDS_PER_RUN = 1000;
var PROPERTY_NAME = 'x_custom.migration.last_id';
var lastId = gs.getProperty(PROPERTY_NAME, '');
gs.info('[Migration] Starting from: ' + (lastId || 'beginning'));
var gr = new GlideRecord('x_custom_source');
if (lastId) {
gr.addQuery('sys_id', '>', lastId);
}
gr.orderBy('sys_id');
gr.setLimit(RECORDS_PER_RUN);
gr.query();
var processed = 0;
var newLastId = '';
while (gr.next()) {
migrateRecord(gr);
processed++;
newLastId = gr.sys_id.toString();
}
if (processed > 0) {
gs.setProperty(PROPERTY_NAME, newLastId);
gs.info('[Migration] Processed ' + processed + ' records. Last ID: ' + newLastId);
// Check if more records exist
var remaining = new GlideAggregate('x_custom_source');
remaining.addQuery('sys_id', '>', newLastId);
remaining.addAggregate('COUNT');
remaining.query();
if (remaining.next()) {
var count = parseInt(remaining.getAggregate('COUNT'));
gs.info('[Migration] ' + count + ' records remaining');
if (count > 0) {
// Schedule next run immediately
scheduleNextRun();
}
}
} else {
gs.info('[Migration] Complete! No more records to process.');
gs.setProperty(PROPERTY_NAME, ''); // Reset for future runs
}
function migrateRecord(record) {
// Migration logic
}
function scheduleNextRun() {
var trigger = new GlideRecord('sys_trigger');
trigger.initialize();
trigger.name = 'Migration Continuation - ' + gs.nowDateTime();
trigger.script = 'gs.include("MigrationScheduler");';
trigger.trigger_type = 0;
trigger.state = 0;
var nextAction = new GlideDateTime();
nextAction.addSeconds(10);
trigger.next_action = nextAction;
trigger.insert();
gs.info('[Migration] Scheduled next run in 10 seconds');
}
active: false
run_type: on_demand
Step 7.2:依存関係チェーンパターン
ほかのジョブの完了を待って実行するジョブを作成します。
MCP の使用:
Tool: SN-Create-Record
Parameters:
table_name: sysauto_script
data:
name: "Job Chain - Step 1: Extract"
script: |
// Step 1: Extract data
gs.info('[Extract] Starting data extraction');
// Your extraction logic
var extractedData = [];
// ... populate extractedData
// Store in temp table for next step
var temp = new GlideRecord('x_custom_job_data');
temp.initialize();
temp.job_name = 'DataSync';
temp.step = 'extract';
temp.data = JSON.stringify(extractedData);
temp.status = 'completed';
temp.insert();
// Trigger next step
var trigger = new GlideRecord('sys_trigger');
trigger.initialize();
trigger.name = 'Job Chain - Step 2: Transform';
trigger.script = 'new x_custom.JobChain().runStep2();';
trigger.trigger_type = 0;
trigger.state = 0;
var nextAction = new GlideDateTime();
nextAction.addSeconds(5);
trigger.next_action = nextAction;
trigger.insert();
gs.info('[Extract] Complete. Triggered Step 2.');
active: true
run_type: daily
run_time: "01:00:00"
Step 7.3:分散処理パターン
大規模な処理を複数のスケジュール済みトリガーに分割します。
// Main job that distributes work
var WORKERS = 5;
var TABLE = 'incident';
var QUERY = 'active=true^needs_processing=true';
// Count total records
var ga = new GlideAggregate(TABLE);
ga.addEncodedQuery(QUERY);
ga.addAggregate('COUNT');
ga.query();
var totalRecords = 0;
if (ga.next()) {
totalRecords = parseInt(ga.getAggregate('COUNT'));
}
if (totalRecords == 0) {
gs.info('[Distributor] No records to process');
return;
}
var batchSize = Math.ceil(totalRecords / WORKERS);
gs.info('[Distributor] Distributing ' + totalRecords + ' records across ' + WORKERS + ' workers');
// Create worker triggers
for (var i = 0; i < WORKERS; i++) {
var trigger = new GlideRecord('sys_trigger');
trigger.initialize();
trigger.name = 'Worker ' + (i + 1) + ' of ' + WORKERS;
trigger.script = 'processWorkerBatch(' + i + ', ' + batchSize + ');';
trigger.trigger_type = 0;
trigger.state = 0;
trigger.next_action = gs.nowDateTime();
trigger.insert();
}
スクリプトテンプレート
テンプレート 1:標準クリーンアップジョブ
// Standard cleanup job template
var JOB_NAME = 'Data Cleanup';
var TABLE = 'your_table';
var QUERY = 'active=false^sys_updated_on<javascript:gs.daysAgo(90)';
var BATCH_SIZE = 500;
var DRY_RUN = true; // Set to false to actually delete
gs.info('[' + JOB_NAME + '] Starting');
var deleted = 0;
var gr = new GlideRecord(TABLE);
gr.addEncodedQuery(QUERY);
gr.setLimit(BATCH_SIZE);
gr.query();
while (gr.next()) {
if (DRY_RUN) {
gs.info('[' + JOB_NAME + '] [DRY RUN] Would delete: ' + gr.sys_id);
} else {
gr.deleteRecord();
}
deleted++;
}
gs.info('[' + JOB_NAME + '] ' + (DRY_RUN ? '[DRY RUN] ' : '') + 'Deleted ' + deleted + ' records');
テンプレート 2:通知ジョブ
// Notification job template
var JOB_NAME = 'Overdue Task Reminder';
var TABLE = 'task';
var QUERY = 'active=true^due_date<javascript:gs.nowDateTime()^state!=3';
gs.info('[' + JOB_NAME + '] Starting');
var notified = 0;
var gr = new GlideRecord(TABLE);
gr.addEncodedQuery(QUERY);
gr.query();
while (gr.next()) {
if (gr.assigned_to.email) {
var email = new GlideEmailOutbound();
email.setSubject('Overdue Task: ' + gr.number);
email.setFrom('[email protected]');
email.addAddress(gr.assigned_to.email);
email.setBody('Your task ' + gr.number + ' is overdue.\n\n' +
'Description: ' + gr.short_description + '\n' +
'Due Date: ' + gr.due_date.getDisplayValue());
email.send();
notified++;
}
}
gs.info('[' + JOB_NAME + '] Sent ' + notified + ' notifications');
テンプレート 3:データ同期ジョブ
// Data sync job template
var JOB_NAME = 'External System Sync';
var SOURCE_TABLE = 'x_custom_staging';
var TARGET_TABLE = 'cmdb_ci_server';
var BATCH_SIZE = 100;
var stats = { created: 0, updated: 0, errors: 0 };
gs.info('[' + JOB_NAME + '] Starting sync');
var gr = new GlideRecord(SOURCE_TABLE);
gr.addQuery('sync_status', 'pending');
gr.setLimit(BATCH_SIZE);
gr.query();
while (gr.next()) {
try {
var target = new GlideRecord(TARGET_TABLE);
target.addQuery('serial_number', gr.serial_number);
target.query();
if (target.next()) {
// Update existing
updateRecord(target, gr);
target.update();
stats.updated++;
} else {
// Create new
target.initialize();
updateRecord(target, gr);
target.insert();
stats.created++;
}
gr.sync_status = 'completed';
gr.sync_date = gs.nowDateTime();
gr.update();
} catch (e) {
gr.sync_status = 'error';
gr.sync_error = e.message;
gr.update();
stats.errors++;
gs.error('[' + JOB_NAME + '] Error: ' + e.message);
}
}
gs.info('[' + JOB_NAME + '] Summary: Created=' + stats.created +
', Updated=' + stats.updated + ', Errors=' + stats.errors);
function updateRecord(target, source) {
target.name = source.hostname;
target.serial_number = source.serial_number;
target.ip_address = source.ip_address;
// Add more field mappings
}
テンプレート 4:監査ジョブ
// Audit job template
var JOB_NAME = 'Security Audit';
var AUDIT_RESULTS = [];
gs.info('[' + JOB_NAME + '] Starting audit');
// Audit 1: Users with admin role but no MFA
var gr = new GlideRecord('sys_user_has_role');
gr.addQuery('role.name', 'admin');
gr.addQuery('user.active', true);
gr.addQuery('user.two_factor_auth', false);
gr.query();
while (gr.next()) {
AUDIT_RESULTS.push({
type: 'admin_no_mfa',
user: gr.user.user_name.toString(),
severity: 'high'
});
}
// Audit 2: Service accounts with interactive login
var serviceAccounts = new GlideRecord('sys_user');
serviceAccounts.addQuery('user_name', 'CONTAINS', 'svc_');
serviceAccounts.addQuery('locked_out', false);
serviceAccounts.addQuery('web_service_access_only', false);
serviceAccounts.query();
while (serviceAccounts.next()) {
AUDIT_RESULTS.push({
type: 'svc_interactive_login',
user: serviceAccounts.user_name.toString(),
severity: 'medium'
});
}
// Log results
gs.info('[' + JOB_NAME + '] Found ' + AUDIT_RESULTS.length + ' findings');
// Store audit results
if (AUDIT_RESULTS.length > 0) {
var auditRecord = new GlideRecord('x_custom_audit_log');
auditRecord.initialize();
auditRecord.audit_type = JOB_NAME;
auditRecord.findings = JSON.stringify(AUDIT_RESULTS);
auditRecord.finding_count = AUDIT_RESULTS.length;
auditRecord.insert();
}
ツールの使用概要
| 操作 | MCP ツール | 目的 |
|---|---|---|
| ジョブの作成 | SN-Create-Record | sysauto_script レコードを作成 |
| ジョブの更新 | SN-Update-Record | ジョブ設定を変更 |
| ジョブのクエリ | SN-Query-Table | ジョブの一覧表示とステータス確認 |
| ログの表示 | SN-Query-Table | syslog をクエリして実行履歴を確認 |
| 直ちに実行 | SN-Execute-Background-Script | ジョブのロジックを直ちに実行 |
| スキーマの取得 | SN-Get-Table-Schema | テーブル構造を把握 |
ベストプラクティス
- 最初にフィルター: 処理が必要なレコードだけに必ず絞り込み、テーブル全体をスキャンしないでください
- 上限を設定: setLimit() を使ってバッチ処理し、タイムアウトを防いでください
- インデックスを考慮: パフォーマンス向上のため、インデックス付きフィールドを使ってクエリを実行してください
- エラー処理: すべてのロジックを適切なログ記録を伴う try-catch で囲んでください
- ドライランモード: 本番環境で実行する前に、DRY_RUN = true に設定してテストしてください
- 詳細なログ記録: ジョブ名のプレフィックスを付けた構造化ログを使用してください
- 実行時間を監視: 実行時間を記録して、時間のかかるジョブを特定してください
- オフピーク時間にスケジュール: 負荷の高いジョブはオフピーク時間(午前 2~5 時)に実行してください
- 条件付き実行: 条件を使って、不要な実行をスキップしてください
- 失敗時に通知: ジョブが失敗した場合は、必ずアラートを送信してください
トラブルシューティング
ジョブが実行されない
症状: スケジュール済みジョブが予定時刻に実行されません 原因:
- ジョブの
active: false - run_time の形式が無効
- 条件スクリプトが false を返す
- ServiceNow のスケジューラーサービスが停止している
解決策:
Tool: SN-Query-Table
Parameters:
table_name: sysauto_script
query: name=Your Job Name
fields: active,run_type,run_time,next_action,conditional,condition
ジョブのタイムアウト
症状: ジョブがタイムアウトエラーで失敗する 原因:
- 処理するレコードが多すぎる
- クエリが非効率
- バッチ数の制限がない
解決策:
- クエリに setLimit() を追加する
- バッチ処理のパターンを使用する
- インデックス付きフィールドでクエリを絞り込む
- 複数の小さなジョブに分割する
ログが表示されない
症状: ジョブは実行されるが、gs.info() の出力が表示されない 原因:
- ログレベルのフィルタリング
- 時間範囲の指定が誤っている
- ログ出力前にスクリプトエラーが発生している
解決策:
Tool: SN-Query-Table
Parameters:
table_name: syslog
query: sys_created_on>javascript:gs.minutesAgo(30)^sourceLIKEScheduled
fields: message,level,source,sys_created_on
limit: 100
ジョブの実行頻度が高すぎる
症状: 定期ジョブが想定より頻繁に実行される 原因:
- run_period の単位が誤っている(秒ではなくミリ秒です!)
- 同じジョブの有効なバージョンが複数ある
解決策:
Tool: SN-Query-Table
Parameters:
table_name: sysauto_script
query: nameLIKEYour Job
fields: name,active,run_period
条件付きジョブが実行されない
症状: 条件付きジョブが毎回スキップされる 原因:
- 条件スクリプトのエラー
- 条件が常に false と評価される
answer変数が設定されていない
解決策: 条件スクリプトを直接テストします:
Tool: SN-Execute-Background-Script
Parameters:
script: |
// Test your condition
var gr = new GlideRecord('your_table');
gr.addQuery('your_query');
gr.setLimit(1);
gr.query();
var answer = gr.hasNext();
gs.info('Condition result: ' + answer);
例
例 1:完全なクリーンアップジョブ
Tool: SN-Create-Record
Parameters:
table_name: sysauto_script
data:
name: "Cleanup Old Attachments"
script: |
var JOB_NAME = 'Attachment Cleanup';
var DAYS_OLD = 365;
var BATCH_SIZE = 100;
gs.info('[' + JOB_NAME + '] Starting');
var cutoff = gs.daysAgo(DAYS_OLD);
var deleted = 0;
var gr = new GlideRecord('sys_attachment');
gr.addQuery('sys_created_on', '<', cutoff);
gr.addQuery('table_name', 'NOT IN', 'kb_knowledge,sys_user');
gr.setLimit(BATCH_SIZE);
gr.query();
while (gr.next()) {
gr.deleteRecord();
deleted++;
}
gs.info('[' + JOB_NAME + '] Deleted ' + deleted + ' attachments');
active: true
run_type: weekly
run_time: "03:00:00"
run_dayofweek: 7
例 2:再試行機能付きの連携ジョブ
Tool: SN-Create-Record
Parameters:
table_name: sysauto_script
data:
name: "External API Sync"
script: |
var JOB_NAME = 'API Sync';
var MAX_RETRIES = 3;
var BATCH_SIZE = 50;
gs.info('[' + JOB_NAME + '] Starting');
var gr = new GlideRecord('x_custom_api_queue');
gr.addQuery('status', 'pending');
gr.addQuery('retry_count', '<', MAX_RETRIES);
gr.orderBy('priority');
gr.setLimit(BATCH_SIZE);
gr.query();
var stats = { success: 0, retry: 0, failed: 0 };
while (gr.next()) {
try {
var result = callExternalAPI(gr);
gr.status = 'completed';
gr.response_data = result;
stats.success++;
} catch (e) {
gr.retry_count++;
gr.last_error = e.message;
if (gr.retry_count >= MAX_RETRIES) {
gr.status = 'failed';
stats.failed++;
} else {
gr.status = 'pending';
stats.retry++;
}
}
gr.update();
}
gs.info('[' + JOB_NAME + '] Results: ' + JSON.stringify(stats));
function callExternalAPI(record) {
var rest = new sn_ws.RESTMessageV2();
rest.setEndpoint('https://api.example.com/sync');
rest.setHttpMethod('POST');
rest.setRequestBody(JSON.stringify({
id: record.external_id.toString(),
data: record.payload.toString()
}));
var response = rest.execute();
if (response.getStatusCode() != 200) {
throw new Error('API error: ' + response.getStatusCode());
}
return response.getBody();
}
active: true
run_type: periodically
run_period: 300000
関連スキル
admin/script-execution- バックグラウンドスクリプトのパターンadmin/batch-operations- レコードの一括操作admin/update-set-management- 更新セットでジョブをキャプチャするitsm/incident-lifecycle- インシデントの自動化