Scheduled Jobs are server-side scripts that run automatically on a schedule — daily cleanups, recurring reports, batch processing, integration syncs. This guide covers how to create and manage them correctly.
Types of Scheduled Work
ServiceNow has several types of scheduled execution:
- Scheduled Script Execution — runs a server-side script on a schedule. Most commonly used.
- Scheduled Import Set — runs a data import on a schedule
- Scheduled Report — generates and emails a report automatically
- Scheduled Flow — triggers a Flow Designer flow on a schedule
Creating a Scheduled Job
Navigate to System Definition > Scheduled Jobs > New. Key fields:
- Run — Daily, Weekly, Monthly, Periodically, Once
- Time — UTC time for the job to run
- Script — the server-side JavaScript to execute
- Run as — which user context to run in (defaults to admin — be specific about this)
- Active — must be checked for the job to run
Basic Scheduled Job Script
// Auto-close stale incidents with no activity for 30 days
var gr = new GlideRecord('incident');
gr.addEncodedQuery(
'active=true^state=2^' +
'sys_updated_on<javascript:gs.daysAgo(30)'
);
gr.query();
var count = 0;
while (gr.next()) {
gr.state = 7; // Closed
gr.close_notes = 'Auto-closed: no activity for 30 days';
gr.setWorkflow(false); // Don't fire Business Rules
gr.update();
count++;
}
gs.log('Auto-closed ' + count + ' stale incidents');
setWorkflow(false) and autoSysFields(false)
In scheduled jobs that process records in bulk, always use gr.setWorkflow(false) unless you explicitly need Business Rules to fire. This prevents:
- Email notifications firing on every record
- Cascading Business Rules running per update
- Significant performance overhead on large batches
If you also don't need the sys_updated_on timestamp to change, add gr.autoSysFields(false) to prevent system fields from updating during the job.
// For silent bulk updates
gr.setWorkflow(false); // No Business Rules
gr.autoSysFields(false); // Don't update sys_updated_on
gr.update();
Batching Large Jobs
Never process millions of records in a single scheduled job run. Use setLimit() and run the job on a short interval instead — process 500 records every 15 minutes rather than 50,000 records once per day. This prevents timeouts and reduces instance load.
// Process 500 records per run — schedule to run every 15 mins
var gr = new GlideRecord('incident');
gr.addEncodedQuery('active=true^u_needs_processing=true');
gr.setLimit(500);
gr.query();
while (gr.next()) {
// Process each record
gr.u_needs_processing = false;
gr.setWorkflow(false);
gr.update();
}
Monitoring Execution
View execution history at System Scheduler > Scheduled Job Log. Each execution shows:
- Start time and end time
- Status (Success, Error, Running)
- Any
gs.log() output from the script
- Error messages if the job failed
If a job shows status Running for longer than expected, it may be stuck. Check System Diagnostics > Running Transactions and cancel if needed.
Performance Considerations
| Issue | Impact | Fix |
| Running during business hours | Slows instance for all users | Schedule for off-peak hours (UTC 02:00–05:00) |
| No setLimit() | Query scans entire table | Always add setLimit() even if you expect few records |
| Business Rules firing | Multiplies execution time | Use setWorkflow(false) for bulk updates |
| No error handling | Silent failures | Wrap script in try/catch, log errors explicitly |
Error Handling Pattern
try {
var gr = new GlideRecord('incident');
gr.addEncodedQuery('active=true^priority=1');
gr.setLimit(100);
gr.query();
var processed = 0;
while (gr.next()) {
try {
// Process individual record
processed++;
} catch (recordError) {
gs.error('Error on record ' + gr.getUniqueValue() +
': ' + recordError.message);
}
}
gs.log('Scheduled job complete. Processed: ' + processed);
} catch (e) {
gs.error('Scheduled job failed: ' + e.message);
}
Common Mistakes
- Using gs.sleep() — never use sleep in scheduled jobs, it holds a database connection open
- Hardcoding sys_ids — use system properties or GlideRecord lookups instead
- No logging — always log start, end, and record counts so you know if the job ran correctly
- Run as admin — create a dedicated service account with only the permissions the job needs
Scheduled Job vs Scheduled Import vs Data Import
ServiceNow has three types of scheduled operations that developers and admins frequently confuse. A Scheduled Job (System Scheduler) runs a Script — server-side JavaScript — on a schedule. A Scheduled Import retrieves a file from a data source (FTP, URL, MID Server directory) and runs it through a Transform Map on a schedule. A Data Import is a manual or API-triggered operation rather than scheduled. Use Scheduled Jobs for any programmatic operation — generating metrics, cleaning up data, triggering integrations, sending digest emails. Use Scheduled Imports for recurring data ingest from external files.
Scheduled Job scripting best practices
// Good Scheduled Job structure
(function() {
var jobName = 'DailyIncidentCleanup';
var startTime = new GlideDateTime();
try {
var processed = 0;
var errors = 0;
var gr = new GlideRecord('incident');
gr.addEncodedQuery('state=6^resolved_atRELATIVELE-30@day@ago');
gr.query();
while (gr.next()) {
try {
gr.setValue('active', false);
gr.setWorkflow(false);
gr.autoSysFields(false);
gr.update();
processed++;
} catch(e) {
errors++;
gs.error(jobName + ': Error processing ' + gr.getValue('number') + ': ' + e.getMessage());
}
}
var elapsed = new GlideDateTime().getNumericValue() - startTime.getNumericValue();
gs.log(jobName + ': Complete. Processed: ' + processed +
', Errors: ' + errors + ', Elapsed: ' + (elapsed/1000).toFixed(1) + 's', jobName);
} catch(e) {
gs.error(jobName + ': Fatal error: ' + e.getMessage(), jobName);
}
})();
Wrapping the entire job script in a self-executing function prevents variable scope pollution. Using try/catch at both the job level and per-record level ensures a single failing record does not abort the entire job. Using setWorkflow(false) and autoSysFields(false) for bulk updates prevents triggering downstream Business Rules and avoids updating sys_updated timestamps unnecessarily.
Batch processing large datasets safely
Scheduled Jobs have a maximum execution time (typically 60 minutes). Jobs that process very large datasets need a batching strategy — process N records per run, track progress, continue where you left off:
// Batched processing pattern using a system property to track progress
var BATCH_SIZE = 500;
var lastProcessed = gs.getProperty('my_job.last_processed_sysid', '');
var gr = new GlideRecord('incident');
gr.addEncodedQuery('active=false^sys_id>' + lastProcessed);
gr.orderBy('sys_id');
gr.setLimit(BATCH_SIZE);
gr.query();
var count = 0;
var lastId = lastProcessed;
while (gr.next()) {
// process record
lastId = gr.getUniqueValue();
count++;
}
// Save progress for next run
gs.setProperty('my_job.last_processed_sysid', lastId);
gs.log('Processed ' + count + ' records. Last processed: ' + lastId, 'BatchJob');
Monitoring scheduled job health
Scheduled Jobs that silently fail are a common source of unexplained data problems — the job ran, no errors were logged, but nothing happened. Build monitoring into your critical jobs: write a completion record to a custom table each run, create a separate monitoring job that alerts if the completion record is not updated within the expected window, and log both the start and end of each job run with the count of records processed. Use the System Log to track job history, and the Scheduled Job History table (sys_trigger_history) to identify jobs that are running long, failing consistently, or not running at all. The Instance Scan does not check scheduled job health, so monitoring is your responsibility.
Related: GlideRecord performance · GlideAggregate · gs object reference · Notification Engine
Worker Threads and Concurrency
Scheduled Jobs run in the background processing thread pool, which is shared with other asynchronous platform tasks. When multiple jobs are configured to run at the same time, they compete for available worker threads. In an instance with heavy usage, a large job that consumes all available threads can delay other jobs and platform processes. For jobs that process large datasets, implementing chunk-based processing — where each run processes a fixed number of records and schedules itself to run again if more remain — provides better thread pool management than a single long-running job. The GlideRecord performance guide covers the query patterns needed to implement efficient chunked processing with offset-based pagination.
Job Monitoring and Failure Alerting
Scheduled Jobs fail silently by default — if a job's script throws an uncaught error, the job run record shows a failed state but no notification is sent. For production jobs that support business-critical processes, add explicit error handling and notification to the job script. A try-catch wrapper that sends a notification to a monitoring group on any exception provides the alerting that the platform does not. For jobs that process records and need to report outcomes, writing a summary to a custom logging table (rather than relying on gs.log, which goes to the syslog and is hard to query) gives operations teams a clean audit trail. Pair this with a Performance Analytics indicator tracking job success/failure counts for trend visibility over time.
Scheduled Script Execution vs Scheduled Flow
ServiceNow offers two mechanisms for scheduled automation: Scheduled Script Execution (traditional Scheduled Job) and Scheduled Flow (Flow Designer schedule trigger). Scheduled Script Execution gives maximum flexibility — you write arbitrary server-side JavaScript with full platform API access. Scheduled Flow is better for process orchestration that needs to call IntegrationHub spokes, send notifications through the standard notification framework, or run the same logic that already exists as a Flow action. For new automations, prefer Scheduled Flow if the logic can be expressed in Flow Designer — the visual representation is easier for non-developers to review and audit, and the built-in error handling framework is more robust than manually implementing try-catch in a script job.
Dependency Management Between Jobs
When one Scheduled Job depends on the completion of another — for example, an aggregation job that should only run after a data-loading job completes — the platform's native scheduling does not enforce this ordering. Two jobs scheduled at the same time may run in any order. Managing dependencies between jobs requires either chaining (the first job explicitly triggers the second on completion using SncTriggerSynchronizer.executeNow()) or time-offsetting (scheduling the dependent job 30 minutes after the first to allow completion). Chaining is more reliable but more complex to implement and maintain. Time-offsetting is simpler but brittle — if the first job runs longer than usual, the second starts on stale data. For complex job dependency graphs, consider implementing a job orchestration table with status tracking, where each job checks whether its prerequisites are complete before executing its main logic.
Want the complete reference?
This article is part of the NowSpectrum knowledge library. Browse all products for cheat sheets, interview prep, and deep-dive reference guides.
Browse All Products →
Free Weekly Newsletter
One practical ServiceNow tip every week.
Written by working professionals. No fluff. Free forever.
Subscribe Free →