Auto-reminder and auto-close after 10 business days for pending approvals and On Hold tickets

Deepika Gangra1
Tera Expert

Hi All,

I have a requirement to implement a scheduled job in ServiceNow for both Incidents and Request Items (RITMs) with the following logic:

Requirement:

  1. For records in:
    • Pending Approval
    • On Hold
  2. The system should:
    • Send reminders during the 10 business days
    • If no action is taken within 10 business days, then:
      • Auto-close the ticket

Business Rules:

  • Business days = Sunday to Thursday
  • Exclude weekends (Friday & Saturday)
  • Exclude holidays (defined in schedule)
  • Applies to:
    • Incident
    • sc_req_item (RITM)

Current Approach:

  • I initially implemented a loop-based logic to calculate business days by excluding weekends
  • However, this does not handle holidays
  • I am now considering using GlideSchedule to:
    • Calculate business duration
    • Handle holidays properly

Challenges:

  • How to accurately calculate 10 business days including holidays
  • How to design reminder logic within those 10 days
  • Ensuring performance efficiency in a scheduled job (avoiding heavy loops per record)
  • Applying the same logic consistently across Incident and RITM

    Scheduled job:
    // ================= DAY CHECK =================
    var gdtToday = new GlideDateTime();
    gdtToday.setStartOfDay();

    var day = parseInt(gdtToday.getDayOfWeekLocalTime(), 10);

    // Run only Sunday–Thursday
    if (day != 5 && day != 6) {

        // ================= CALCULATE 10 BUSINESS DAYS =================
        var count = 0;
        var pastDate = new GlideDateTime(gdtToday);

        while (count < 10) {

            pastDate.addDaysLocalTime(-1);

            var d = parseInt(pastDate.getDayOfWeekLocalTime(), 10);

            // Skip Friday (5) and Saturday (6)
            if (d != 5 && d != 6) {
                count++;
            }
        }

        // VERY IMPORTANT → include full day
        pastDate.setDisplayValue(pastDate.getLocalDate() + " 23:59:59");

        gs.info("Auto-close running. Cutoff date: " + pastDate.getDisplayValue());

        // ================= INCIDENT =================
        var appr = new GlideRecord('incident');
        appr.addQuery('active', true);
        appr.addQuery('state', 3);
        appr.addQuery('sys_updated_on', '<=', pastDate);
        appr.addQuery('hold_reason', '6');
        appr.query();

        var incidentCount = 0;

        while (appr.next()) {

            gs.eventQueue('autocancel.incident', appr, "", "");

            appr.state = '8';
            appr.sys_updated_by = 'system';
            appr.comments = "This ticket has been automatically closed due to no activity over the past 10 business days (Sun–Thu). On Hold Reason: " + appr.getDisplayValue('hold_reason');

            appr.update();
            incidentCount++;
        }

        // ================= RITM =================
        var gr_SRonhold = new GlideRecord('sc_req_item');
        gr_SRonhold.addQuery('active', true);
        gr_SRonhold.addQuery('state', 17);
        gr_SRonhold.addQuery('sys_updated_on', '<=', pastDate);
        gr_SRonhold.addQuery('u_on_hold_reason', '2');
        gr_SRonhold.query();

        var ritmCount = 0;

        while (gr_SRonhold.next()) {

            var ritmSysId = gr_SRonhold.sys_id;

            // Reject approvals
            var notapproved = new GlideRecord('sysapproval_approver');
            notapproved.addQuery('source_table', 'sc_req_item');
            notapproved.addQuery('sysapproval', ritmSysId);
            notapproved.addQuery('state', 'requested');
            notapproved.query();

            while (notapproved.next()) {
                notapproved.state = 'rejected';
                notapproved.comments = "Auto-closed after 10 business days. On Hold Reason: " + gr_SRonhold.getDisplayValue('u_on_hold_reason');
                notapproved.update();
            }

            // Close RITM
            gr_SRonhold.state = '4';
            gr_SRonhold.comments = "This ticket has been automatically closed due to no activity over the past 10 business days. On Hold Reason: " + gr_SRonhold.getDisplayValue('u_on_hold_reason');
            gr_SRonhold.update();

            // Update Request
            var grReq = new GlideRecord('sc_request');
            grReq.addQuery('sys_id', gr_SRonhold.request);
            grReq.query();

            while (grReq.next()) {
                grReq.state = '4';
                grReq.update();
            }

            // Update Catalog Tasks
            var grtask = new GlideRecord('sc_task');
            grtask.addQuery('request_item', ritmSysId);
            grtask.query();

            while (grtask.next()) {
                grtask.state = '4';
                grtask.update();
            }

            gs.eventQueue('autocancel.requestItem', gr_SRonhold, "", "");
            ritmCount++;
        }

        // ================= LOG =================
        gs.info("Auto-close completed. Incidents: " + incidentCount + ", RITMs: " + ritmCount);

    } else {

        gs.info("Auto-close job skipped (Weekend)");

    }
3 REPLIES 3

Its_Azar
Mega Sage

Hi there @Deepika Gangra1 

 

Right now your logic only excludes Fri/Sat, so holidays will still break the calculation.

I’d recommend:

  • Create a business schedule (Sun–Thu + holidays)
  • Use DurationCalculator to check if 10 business days elapsed
  • Run reminders separately (ex: day 3, 7, 9)
  • Batch process records to avoid huge loops in scheduled jobs

Also, small optimization:
Instead of querying Requests and Tasks inside every RITM loop, consider using bulk updates or encoded queries to reduce DB hits.

☑️ If this helped, please mark it as Helpful or Accept Solution so others can find the answer too.

Kind Regards,
Azar
Serivenow Rising Star
Developer @ KPMG.

Hi @Its_Azar ,

Thanks for your suggestion, it’s really helpful.

In my case, the requirement is slightly different — I need to send reminders continuously for all 10 business days (Sun–Thu, excluding weekends and holidays) for records in Pending Approval and On Hold state.
If there is still no action after the 10th business day, the ticket should be auto-closed.

So instead of sending reminders only on specific days (like Day 3, 7, 9), the expectation is to trigger reminders daily during the 10 business day window.

Could you please help with a sample script or best approach using DurationCalculator or GlideSchedule to handle this efficiently (especially avoiding loops and handling holidays correctly)?

Also, any recommendations on avoiding duplicate reminders (since this will run as a scheduled job) would be really helpful.

Thanks in advance!

vaishali231
Kilo Sage

Hey @Deepika Gangra1 

The better approach is:

  1. Create a schedule for Sunday to Thursday.
  2. Add holidays to that schedule.
  3. Use GlideSchedule to calculate the business duration.
  4. Store reminder tracking fields on the record to avoid duplicate reminders.

Script 

(function executeAutoCloseReminderJob() {

    var scheduleSysId = 'PUT_SCHEDULE_SYS_ID_HERE';
    var schedule = new GlideSchedule(scheduleSysId);

    var tenBusinessDaysMs = 10 * 24 * 60 * 60 * 1000;
    var now = new GlideDateTime();

    if (!schedule.isInSchedule(now)) {
        gs.info('Auto-close reminder job skipped because current time is outside business schedule.');
        return;
    }

    processIncidents(schedule, now, tenBusinessDaysMs);
    processRITMs(schedule, now, tenBusinessDaysMs);

    function processIncidents(schedule, now, tenBusinessDaysMs) {

        var inc = new GlideRecord('incident');
        inc.addQuery('active', true);
        inc.addQuery('state', '3'); // On Hold
        inc.addQuery('hold_reason', '6');
        inc.addQuery('u_auto_close_processed', '!=', true);
        inc.query();

        while (inc.next()) {

            if (!inc.u_pending_hold_start) {
                inc.u_pending_hold_start = inc.sys_updated_on;
                inc.update();
                continue;
            }

            var start = new GlideDateTime(inc.u_pending_hold_start);
            var duration = schedule.duration(start, now);
            var elapsedMs = duration.getNumericValue();

            if (elapsedMs >= tenBusinessDaysMs) {
                inc.state = '8'; // Closed / Cancelled as per your instance
                inc.comments = 'Ticket auto-closed because no action was taken within 10 business days.';
                inc.u_auto_close_processed = true;
                inc.update();

                gs.eventQueue('autocancel.incident', inc, '', '');
                continue;
            }

            if (isReminderAlreadySentToday(inc, now)) {
                continue;
            }

            gs.eventQueue('autocancel.incident.reminder', inc, '', '');

            inc.u_last_reminder_sent = now;
            inc.u_reminder_count = parseInt(inc.u_reminder_count || 0, 10) + 1;
            inc.update();
        }
    }

    function processRITMs(schedule, now, tenBusinessDaysMs) {

        var ritm = new GlideRecord('sc_req_item');
        ritm.addQuery('active', true);
        ritm.addQuery('state', '17'); // Pending Approval / On Hold as per your instance
        ritm.addQuery('u_on_hold_reason', '2');
        ritm.addQuery('u_auto_close_processed', '!=', true);
        ritm.query();

        while (ritm.next()) {

            if (!ritm.u_pending_hold_start) {
                ritm.u_pending_hold_start = ritm.sys_updated_on;
                ritm.update();
                continue;
            }

            var start = new GlideDateTime(ritm.u_pending_hold_start);
            var duration = schedule.duration(start, now);
            var elapsedMs = duration.getNumericValue();

            if (elapsedMs >= tenBusinessDaysMs) {

                rejectPendingApprovals(ritm);

                ritm.state = '4'; // Closed Incomplete / Cancelled as per your instance
                ritm.comments = 'RITM auto-closed because no action was taken within 10 business days.';
                ritm.u_auto_close_processed = true;
                ritm.update();

                closeCatalogTasks(ritm.sys_id);
                closeParentRequest(ritm.request);

                gs.eventQueue('autocancel.requestItem', ritm, '', '');
                continue;
            }

            if (isReminderAlreadySentToday(ritm, now)) {
                continue;
            }

            gs.eventQueue('autocancel.ritm.reminder', ritm, '', '');

            ritm.u_last_reminder_sent = now;
            ritm.u_reminder_count = parseInt(ritm.u_reminder_count || 0, 10) + 1;
            ritm.update();
        }
    }

    function isReminderAlreadySentToday(gr, now) {

        if (!gr.u_last_reminder_sent) {
            return false;
        }

        var lastReminder = new GlideDateTime(gr.u_last_reminder_sent);

        return lastReminder.getLocalDate().toString() == now.getLocalDate().toString();
    }

    function rejectPendingApprovals(ritm) {

        var appr = new GlideRecord('sysapproval_approver');
        appr.addQuery('sysapproval', ritm.sys_id);
        appr.addQuery('state', 'requested');
        appr.query();

        while (appr.next()) {
            appr.state = 'rejected';
            appr.comments = 'Approval rejected because the RITM was auto-closed after 10 business days.';
            appr.update();
        }
    }

    function closeCatalogTasks(ritmSysId) {

        var task = new GlideRecord('sc_task');
        task.addQuery('request_item', ritmSysId);
        task.addQuery('active', true);
        task.query();

        while (task.next()) {
            task.state = '4';
            task.update();
        }
    }

    function closeParentRequest(requestSysId) {

        if (!requestSysId) {
            return;
        }

        var req = new GlideRecord('sc_request');

        if (req.get(requestSysId)) {
            req.state = '4';
            req.update();
        }
    }

})();

*************************************************************************************************************************************

If this response helps, please mark it as Accept as Solution and Helpful.

Doing so helps others in the community and encourages me to keep contributing.

Regards

Vaishali Singh

Servicenow Developer
Linkedin - https://www.linkedin.com/in/vaishali-singh-2273361bb