mcpField
S
scheduled-jobs

scheduled-jobs

github.com/happy-technologies-llc開発4144 回インストール4 日前に更新
リポジトリ

scheduled-jobs について

ServiceNow の開発者と管理者向けに、スケジュールされたバックグラウンドジョブの作成と管理を扱うスキルです。実行条件や性能、エラー処理、監視について説明します。

主な機能

  1. 01定期実行・単発ジョブの作成
  2. 02実行頻度と条件の設定
  3. 03フィルターとバッチによる処理の最適化
  4. 04エラー処理とイベント通知
  5. 05ログの検索とジョブの監視

活用シーン

  1. 01定期的なレコード整理や処理
  2. 02レポートやデータ同期のスケジュール実行
  3. 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:インデックスを考慮したクエリ

クエリではインデックス付きフィールドを使用してください。一般的なインデックス付きフィールドは次のとおりです。

テーブルインデックス付きフィールド
incidentnumber, sys_id, active, state, priority, assigned_to, assignment_group
tasknumber, sys_id, active, state, assigned_to, assignment_group
sys_usersys_id, user_name, email, active
sys_user_groupsys_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

トリガータイプの値:

値説明
01 回実行
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-Recordsysauto_script レコードを作成
ジョブの更新SN-Update-Recordジョブ設定を変更
ジョブのクエリSN-Query-Tableジョブの一覧表示とステータス確認
ログの表示SN-Query-Tablesyslog をクエリして実行履歴を確認
直ちに実行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 - インシデントの自動化

参考資料

コメント