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)——一次性或有条件运行的任务
  • 运行频率模式——每日、每周、每月和 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系统触发器(一次性/系统事件)name, script, next_action, trigger_type, state
syslog执行日志message, level, source, sys_created_on
sys_script脚本包含项(可复用逻辑)name, script, active, api_name
sysauto_script 的运行类型
运行类型值说明
运行一次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:00(使用 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
}
第 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
第 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'));
}
第 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 阶段:错误处理和通知
第 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
第 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 阶段:监控和调试
第 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
第 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
第 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 阶段:按需执行
第 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
第 6.2 步:创建一次性触发器

使用 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定期运行
2由事件触发
第 7 阶段:高级模式
第 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
第 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"
第 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 - 事件自动化

参考资料

评论

支持 Markdown