Scheduled Job Development
SkillProductivityComprehensive guide to creating and managing ServiceNow scheduled jobs - run frequencies, conditional execution, performance optimization, error handling, and debugging
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Scheduled Job Development skill
What this skill tells your AI
The instructions your AI receives, as published by happy-technologies-llc/happy-platform-skills in skills/development/scheduled-jobs/SKILL.md and read by ahel’s review.
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');
Shortened here. Read the whole file on GitHub.
Signals
- GitHub stars
- 37
- Forks
- 13
- Last commit
- Jul 2026
Advanced
- Catalog kind
- skill
- Gateway key
scheduled-jobs- Source
- github.com/happy-technologies-llc/happy-platform-skills