scheduled-jobs
Scheduled 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('servicenow@company.com');
email.addAddress('it-security@company.com');
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('servicenow@company.com');
email.addAddress('it-alerts@company.com');
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('servicenow@company.com');
email.addAddress('it-reports@company.com');
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('servicenow@company.com');
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
References
More from happy-technologies-llc/happy-platform-skills
happy-platform-skills
Reusable development patterns and automation recipes for enterprise platforms - 180+ skills across 23 categories
17flow-generation
Generate ServiceNow Flow Designer flows from natural language descriptions including triggers, actions, conditions, subflows, approval flows, notification flows, and data manipulation flows
4application-scope
Manage scoped application development including setting application context and update set alignment
4scripted-rest-apis
Comprehensive guide to creating, securing, and testing Scripted REST APIs in ServiceNow for custom integrations and external system connectivity
4automated-testing
Comprehensive Automated Test Framework (ATF) guide for creating, managing, and executing automated tests in ServiceNow
4analytics-generation
Generate analytics dashboards and visualizations from natural language descriptions covering PA indicators, data collectors, and widgets
4