About scheduled-jobs
A skill for ServiceNow developers and administrators creating and managing scheduled background jobs. It covers scheduling, conditional runs, performance, error handling, and monitoring.
Key features
- 01Create recurring and one-time jobs
- 02Set run schedules and conditions
- 03Optimize processing with filters and batches
- 04Handle errors and event notifications
- 05Query logs and monitor jobs
Use cases
- 01Schedule recurring record cleanup or processing
- 02Run reports or data synchronization on a schedule
- 03Troubleshoot jobs that fail to run or get stuck
MCP support
Examples use MCP tools to create and query ServiceNow scheduled jobs and logs, and to run background scripts.
Install
Copy the command and run it in a terminal.
npx skills add https:/ / github.com/ happy-technologies-llc/ happy-platform-skills --skill scheduled-jobsAfter installing, type/scheduled-jobsin Claude Code to confirm it loaded.
Details
From the project READMEScheduled Job Development
Overview
This skill covers creating and managing ServiceNow scheduled jobs for automated background processing:
- Scheduled Script Executions (
sysauto_script) - Recurring jobs with cron-like scheduling - System Triggers (
sys_trigger) - One-time or conditional job executions - Run frequency patterns - Daily, weekly, monthly, and cron expressions
- Conditional execution - Run only when specific conditions are met
- Performance optimization - Filter to records needing processing (critical!)
- Error handling - Robust error management and notifications
- Monitoring and debugging - Track execution history and troubleshoot issues
When to use: When you need to automate recurring tasks, batch processing, data cleanup, notifications, or any background operations that run on a schedule.
Who should use this: Developers and administrators who need to automate ServiceNow operations without user intervention.
Prerequisites
- Roles:
admin(required for scheduled job creation and management) - Access:
sysauto_script,sys_trigger,syslogtables - Knowledge: GlideRecord API, ServiceNow server-side JavaScript
- Related Skills:
admin/script-execution- Background script patternsadmin/batch-operations- Bulk record operations
Table Architecture
Core Tables
| Table | Purpose | Key Fields |
|---|---|---|
sysauto_script | Scheduled script executions (recurring) | name, script, run_type, run_time, conditional |
sys_trigger | System triggers (one-time/system events) | name, script, next_action, trigger_type, state |
syslog | Execution logs | message, level, source, sys_created_on |
sys_script | Script includes (reusable logic) | name, script, active, api_name |
Run Types for sysauto_script
| Run Type | Value | Description |
|---|---|---|
| Run Once | on_demand | Manual execution only |
| Daily | daily | Runs every day at specified time |
| Weekly | weekly | Runs on specified day(s) of week |
| Monthly | monthly | Runs on specified day of month |
| Periodically | periodically | Runs at fixed intervals |
| On Demand | on_demand | Manual trigger only |
Procedure
Phase 1: Create Scheduled Script Execution
Step 1.1: Basic Daily Job
Create a scheduled job that runs daily.
Using 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
Key Fields Explained:
run_type: daily- Runs every dayrun_time: "02:00:00"- 2:00 AM (use 24-hour format)run_dayofweek: 1- Monday (1=Mon, 2=Tue, ..., 7=Sun) - used for weekly
Step 1.2: Weekly Job
Create a job that runs every Monday at 6 AM.
Using 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
Step 1.3: Monthly Job
Create a job that runs on the 1st of each month.
Using 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
Step 1.4: Periodic Job (Every N Minutes/Hours)
Create a job that runs every 15 minutes.
Using 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
Note: run_period is in milliseconds (900000 = 15 minutes)
| Interval | Milliseconds |
|---|---|
| 1 minute | 60000 |
| 5 minutes | 300000 |
| 15 minutes | 900000 |
| 30 minutes | 1800000 |
| 1 hour | 3600000 |
| 4 hours | 14400000 |
| 12 hours | 43200000 |
Phase 2: Conditional Execution
Step 2.1: Conditional Job (Run Only When Condition Met)
Create a job that only runs if there are records to process.
Using 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();
Key Fields:
conditional: true- Enables condition checkingcondition- Script that setsanswer = true/false
Step 2.2: Business Hours Only
Create a job that only runs during business hours.
Using 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;
Phase 3: Performance Optimization
CRITICAL: Scheduled jobs must be optimized to avoid performance issues. The most common mistake is processing ALL records instead of filtering to only those needing work.
Step 3.1: Efficient Query Pattern
BAD Pattern (Full Table Scan):
// 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
}
}
GOOD Pattern (Filtered Query):
// 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: Batch Processing Pattern
Using 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: Use Aggregates for Counting
BAD Pattern (Loads All Records):
var gr = new GlideRecord('incident');
gr.addQuery('active', true);
gr.query();
var count = gr.getRowCount(); // Loads all records first!
GOOD Pattern (Uses Aggregate):
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: Index-Aware Queries
Ensure your queries use indexed fields. Common indexed fields:
| Table | Indexed Fields |
|---|---|
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 |
Verify Indexes:
Tool: SN-Query-Table
Parameters:
table_name: sys_index
query: table=incident
fields: name,columns,active
Phase 4: Error Handling and Notifications
Step 4.1: Comprehensive Error Handling
Using 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: Event-Based Notifications
Use ServiceNow events for notifications instead of direct email.
Create Event Registration:
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"
Use in Script:
// In your scheduled job script
try {
// Processing logic
} catch (e) {
gs.eventQueue('scheduled.job.failed', null, JOB_NAME, e.message);
}
Phase 5: Monitoring and Debugging
Step 5.1: Query Job Execution History
Find Recent Executions:
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
Find Specific Job Logs:
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
Find Error Logs:
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: Check Job Status
List All Active Scheduled Jobs:
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
Check Specific Job:
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: Debug Running Jobs
Check System Triggers:
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
Check for Stuck Jobs:
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
Phase 6: On-Demand Execution
Step 6.1: Execute Job Immediately
Using 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: Create One-Time Trigger
Using 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
Trigger Type Values:
| Value | Description |
|---|---|
| 0 | Run once |
| 1 | Run periodically |
| 2 | On event |
Phase 7: Advanced Patterns
Step 7.1: Self-Scheduling Pattern
For very long-running operations, chain multiple executions.
Using 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: Dependency Chain Pattern
Create jobs that depend on other jobs completing.
Using 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: Distributed Processing Pattern
Split large workloads across multiple scheduled triggers.
// 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();
}
Script Templates
Template 1: Standard Cleanup Job
// 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');
Template 2: Notification Job
// 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');
Template 3: Data Sync Job
// 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
}
Template 4: Audit Job
// 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();
}
Tool Usage Summary
| Operation | MCP Tool | Purpose |
|---|---|---|
| Create job | SN-Create-Record | Create sysauto_script record |
| Update job | SN-Update-Record | Modify job settings |
| Query jobs | SN-Query-Table | List and check job status |
| View logs | SN-Query-Table | Query syslog for execution history |
| Execute now | SN-Execute-Background-Script | Run job logic immediately |
| Get schema | SN-Get-Table-Schema | Understand table structure |
Best Practices
- Filter First: Always filter to records needing processing - never scan entire tables
- Set Limits: Use setLimit() to process in batches and prevent timeout
- Index Awareness: Query on indexed fields for performance
- Error Handling: Wrap all logic in try-catch with proper logging
- Dry Run Mode: Test with DRY_RUN = true before production execution
- Log Extensively: Use structured logging with job name prefix
- Monitor Duration: Log execution time to identify slow jobs
- Off-Peak Scheduling: Run intensive jobs during off-peak hours (2-5 AM)
- Conditional Execution: Use conditions to skip unnecessary runs
- Notification on Failure: Always send alerts when jobs fail
Troubleshooting
Job Not Running
Symptom: Scheduled job doesn't execute at expected time Causes:
active: falseon job- Invalid run_time format
- Condition script returns false
- ServiceNow scheduler service stopped
Solution:
Tool: SN-Query-Table
Parameters:
table_name: sysauto_script
query: name=Your Job Name
fields: active,run_type,run_time,next_action,conditional,condition
Job Timeout
Symptom: Job fails with timeout error Causes:
- Processing too many records
- Inefficient queries
- No batch limiting
Solution:
- Add setLimit() to queries
- Use batch processing pattern
- Filter queries to indexed fields
- Break into multiple smaller jobs
No Logs Appearing
Symptom: Job runs but no gs.info() output visible Causes:
- Log level filtering
- Looking in wrong time range
- Script error before logging
Solution:
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
Job Runs Too Frequently
Symptom: Periodic job runs more often than expected Causes:
- run_period in wrong units (milliseconds, not seconds!)
- Multiple active versions of same job
Solution:
Tool: SN-Query-Table
Parameters:
table_name: sysauto_script
query: nameLIKEYour Job
fields: name,active,run_period
Conditional Job Never Runs
Symptom: Conditional job skipped every time Causes:
- Condition script error
- Condition always evaluates to false
answervariable not set
Solution: Test condition script directly:
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);
Examples
Example 1: Complete Cleanup Job
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
Example 2: Integration Job with Retry
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
Related Skills
admin/script-execution- Background script patternsadmin/batch-operations- Bulk record operationsadmin/update-set-management- Capture jobs in update setsitsm/incident-lifecycle- Incident automation