Automating the closure of outstanding requested item approvals and sending approval reminders

Clinton Arendt
Tera Contributor

Foreword: This is a solution that worked for our company. It may not work in your environment. As always, develop and test before going live. Also, we link our approvals to our requested items, if your company uses a different method, adjustments will be required.

 

Background: Our company requested a solution to the growing number of requested items (RITM) that were piling up unapproved and not able to be progressed or closed out without notifying our customers and approvers.

 

Old Work Around

We already had a very simple workaround for re-sending outstanding email notifications: A UI Action called Resend Approval Email, which allows users who can see the approval record and it is in the state of requested, to fire an event, in turn triggering an email notification (code sample below). The email notification is a simple variation of the original approval email with our company's branding. The email is also logged against the original approval request, which is great for keeping track of how many times we've sent out reminders.

 

resendApprovalEmail();
 
function resendApprovalEmail() {
gs.log("Sending Resend Approval Event");
gs.eventQueue("approval.resend", current, "", "");
gs.addInfoMessage("Resending approval email to: " + current.approver.email);
}

 

However, this cannot address the issue where we have the entire company submitting requests, and each day they age out further. So @David House and myself worked on a solution to perform two functions:

  • Provide our IT Service Desk with a button to gracefully close out a RITM without notifying the customer or approver
  • Automate an approval reminder and cancellation flow that:
    • Reminds approvers of pending request item approvals as they age to 3, 7, 14 and 29 days
    • and cancel the request item out without notifying the customer or approver on the 30th day.

IT Service Desk Button

ClintonArendt_0-1692324720087.png

ClintonArendt_1-1692324729791.png

The button is visible on requested items, visible only to roles 'admin' and 'Service Desk Team', and when the requested item’s approval state is Requested. The code in the UI Action works around the mandatory fields on the requested item and references the flow script action that I will go into detail about later on.

 

//Client-side 'onclick' function
function runCancelonClient() {
    if (confirm("Are you sure you want to cancel this request?")) {
        g_form.addInfoMessage("Cancelling the request..."); // Optional: Show an info message
        g_form.setMandatory('assignment_group', false);
        //Call the UI Action and skip the 'onclick' function
        gsftSubmit(null, g_form.getFormElement(), 'tcc_ritm_cancel'); //MUST call the 'Action name' set in this UI Action	
    }
}
//Code that runs without 'onclick'
//Ensure call to server-side function with no browser errors
if (typeof window == 'undefined')
    runCancelOnServer();

//Server-side function
function runCancelOnServer() {
    (function() {

        try {
            var inputs = {};
            inputs['requested_item'] = current; // GlideRecord of table: sc_req_item 

            // Start Asynchronously: Uncomment to run in background. Code snippet will not have access to outputs.
            // sn_fd.FlowAPI.getRunner().action('global.tcc_cancel_request_item_without_notification').inBackground().withInputs(inputs).run();

            // Execute Synchronously: Run in foreground. Code snippet has access to outputs.
            var result = sn_fd.FlowAPI.getRunner().action('global.tcc_cancel_request_item_without_notification').inForeground().withInputs(inputs).run();
            var outputs = result.getOutputs();

            // Current action has no outputs defined.		
        } catch (ex) {
            var message = ex.getMessage();
            gs.error(message);
        }

    })();
    action.setRedirectURL(current);
}

 

Automated solution

Base items

  • Flow - (TCC) Request Items awaiting approval - Daily
  • Subflow - (TCC) Request Item Approval Reminder
  • Email Notifications
    • (TCC) Approval Resend - Flow 3 day
    • (TCC) Approval Resend - Flow 7 day
    • (TCC) Approval Resend - Flow 14 day
    • (TCC) Approval Resend - Flow 29 day
  • Flow Script Action - (TCC) Cancel Request Item without notification

Flow - (TCC) Request Items awaiting approval - Daily

Runs daily at 04:00

Performs a look up for all approval records where:

  • State = Requested
  • Approval for.Task type = Requested item
  • Created relative before 2 days ago
  • Created relative after 4 days ago

The relative days created is isolating the approval records to created 3 days ago.

It then passes each request item to the subflow as an input as well as the day value. In order to send the appropriate email notification (see the subflow below).

This filter and subflow process is repeated for 7 days, 14 days and 29 days.

For the final lookup it locates all requested items that are at or beyond 30 days of age and passes those requested item records to the script action.

ClintonArendt_1-1692332101780.png

3 day filter example

ClintonArendt_3-1692332178396.png

Subflow - (TCC) Request Item Approval Reminder

Our emails for each day iteration simply advise the Approver and Requested by that this is the Nth day reminder, there will be however many more remaining days of reminders, until the 30th day when it will be automatically closed.

ClintonArendt_2-1692332138711.png

Flow Script Action - (TCC) Cancel Request Item without notification

The most complicated part of the entire process is this flow script action.

Taking in the requested item it delicately cancels any related flows and workflows. Noting that if a flow is found there is an extensive wait included to cater for delayed subflows cancelling. 

It then cancels approval workflows and sets the approval request to 'No longer required'. Noting that this does nothing for catalogue items running on workflows, their approvals will still show as cancelled.

It does the same for group approvals.

Then it adjusts the values of the requested item and finally the request.

 

This script will prevent any tasks from being created, emails from being fired, or request items from being assigned.

 

And an additional reminder, the UI Action for 'Cancel (No notification)' from earlier references this flow script action. Without it it will not function.

 

Input: Request Item

 

(function execute(inputs, outputs) {
  var requestSysID = "";
  // Request Item canceller - Cancelling occurs at the end
  // While not strictly required it does error check for if the record is manipulated after the button press or the automated schedule has started
  var grRequestItem = new GlideRecord("sc_req_item");
  grRequestItem.addQuery("sys_id", inputs.requested_item.sys_id);
  grRequestItem.query();
  if (grRequestItem.next()) {
	// Flow canceller - only runs if flow contexts are present
    var flowFound = false; // Boolean variable used to if a flow is found and a 15 second wait is needed
    var flowContext = new GlideRecord("sys_flow_context");
    flowContext.addQuery("source_record", grRequestItem.sys_id);
    flowContext.query();
    while (flowContext.next()) {
      sn_fd.FlowAPI.cancel(flowContext.sys_id,""); // sys_id of the current record's flow (view this from the flow execution list)
      flowFound = true;
    }
    if (flowFound == true) {
    	gs.sleep(15000); // Required lengthy 15 second wait for flows with subflows that fail to close out quickly
    }
    
    // Workflow canceller - only runs if workflow contexts are present
    var workflowContext = new GlideRecord("wf_context");
    workflowContext.addQuery("id", grRequestItem.request.sys_id.toString());
    workflowContext.query();
    while (workflowContext.next()) {
      new Workflow().cancelContext(workflowContext);
    }
    
    // Approval canceller
    // Locate every approval and set to - No Longer Required
    // Note: Less relevant for Workflows, as cancelling them sets their approvals to Cancelled
    var grApprovalRecord = new GlideRecord("sysapproval_approver");
    grApprovalRecord.addQuery("document_id", grRequestItem.sys_id);
    grApprovalRecord.query();
    while (grApprovalRecord.next()) {
      if (grApprovalRecord.state == "requested") {
        new Workflow().cancel(grApprovalRecord); // Cancel approval workflow
        grApprovalRecord.setWorkflow(false); // Prevents sending of emails
        grApprovalRecord.state = "not_required";
        grApprovalRecord.update();
      }
    }
    
    // Group approval canceller
    // Locate every Group approval and set to - No Longer Required
    var grGroupApprovalRecord = new GlideRecord("sysapproval_group");
    grGroupApprovalRecord.addQuery("parent", grRequestItem.sys_id);
    grGroupApprovalRecord.query();
    while (grGroupApprovalRecord.next()) {
      if (grGroupApprovalRecord.approval == "requested") {
        new Workflow().cancel(grGroupApprovalRecord); // Cancel group approval workflow
        grGroupApprovalRecord.setWorkflow(false); // Prevents sending of emails
        grGroupApprovalRecord.approval = "not_required";
        grGroupApprovalRecord.update();
      }
    }
    
    // Request Item cancellation
	requestSysID = grRequestItem.request.toString();
    grRequestItem.approval = "Cancelled";
    grRequestItem.stage = "Request Cancelled";
    grRequestItem.state = "7";
    grRequestItem.active = "false";
    grRequestItem.update();
  }

  // Request canceller
  var grRequest = new GlideRecord("sc_request");
  grRequest.addQuery("sys_id", requestSysID);
  grRequest.query();
  if (grRequest.next()) {
    grRequest.setWorkflow(false); // Cancel request workflow
    grRequest.approval = "rejected";
    grRequest.request_state = "closed_cancelled";
    grRequest.update();
  }
})(inputs, outputs);

 

Conclusion: There are likely better ways to do this but this has worked for us.

0 REPLIES 0