Using SLA to Set Due Date Based on Ticket Status

I would like to use SLA’s in a queue, but not necessarily in the generic sense where an agent is held accountable to respond to a customer in a timely manner. This is an internal queue and there is no correspondence with any external parties. The idea is to hold the responsible agent (owner) accountable with ensuring the ticket progresses according to the designed workflow in a timely fashion.

For example, if an owner doesn’t move the ticket to the next status within a certain time frame, the ticket gets reassigned to a new owner to ensure follow up on it. rtcrontool is the perfect way to perform these automated actions, however I want them to be done according to SLA Business Hours so that the ticket owners are held accountable within reason of the company’s business hours.

Running rtcrontool only during business hours doesn’t solve the issue. Say an owner has a full business day (8 hours) to change that status of a “new” ticket to “open” otherwise it is reassigned. Say the ticket comes in at 15:00 on Friday and the company Business Hours are M-F 9:00-18:00. Even if rt-crontool doesn’t run on the weekends, rt-crontool will run again at 9:00 on Monday and determine it has been over 24 hours and reassign the ticket, which is no good. I want rtcrontool to respect the SLA Business Hours and determine that only 3 business hours have passed between Friday 15:00 and Monday 9:00; the ticket would be reassigned after 8 business hours have passed, which would be Monday 15:00.

I can’t find any documentation indicating that rt-crontool has native support to respect SLA’s. I’m guessing custom conditions may need to be written for this, but that is always challenging for me. Not to say I won’t try.

Can anyone provide any suggestions or tips?

Thanks in Advance!

Does no one else have the need to automate tasks with rt-crontool according to business hours? Maybe there’s an obvious solution here that I’m missing. Can anyone help to point me in the right direction?

Thanks!

If you’re using SLA’s for starts/due dates then I would think you could have rt-crontool perform searches based on those values and then perform some action on any tickets found from that search.

Hi Craig,

Good thought. However, I think the start/due dates are manipulated only by correspondence in the ticket, whereas my workflow is based on the ticket progressing through the different statuses of the lifecycle, and not related to correspondence at all.

Perhaps I could have custom scrips to update the Due date field based on the actions taken in the ticket. I would need to reference the SLA logic in order to make sure the scrips are setting the Due date based on Business Hours. Can you help me understand what my scrips would need to reference in order to use the SLA logic? This way rt-crontool doesn’t need to include logic to work around business hours; I like that concept.

Thank you!

I am not 100% on this as I haven’t tested anything, but you could try making a new scrip that runs on status change:

Condition: On Status Change
Action: Set the Due date accordingly to SLA
Template: Blank

This a good start. The only downside is each part of the lifecycle has a differing SLA. One status may be 1 business day, another status may be 3 business days, etc. Perhaps this calls for a custom Scrip Action to define some logic for this. Thoughts?

Thanks!

You will need some custom solution, maybe a new action as you suggested. If you look at the existing action lib/RT/Action/SLA_SetDue.pm you can see how it determines what the due date should be set as based off of the ‘level’ of SLA.

You would need something very similar but it would be based off of the status of the ticket instead of the SLA level. You could use a config hash like SLA uses, and call it inside your action.

Thank you Craig. I’ll take a look at the code and see what I can do with it!

I’d love to hear if you had any progress with this, I’ve a similar issue I’m hoping to address Search based on BusinessDays

Hi Craig,

I’m finally getting around to look into this. Hopefully you can help me navigate around this a bit. With config hash, you mean values in the SLA configuration that is set by me in RT_SiteConfig.pm? I think this would make the most sense, but I’m not really sure how RT reads these values from the config. I think the config example would look something like this:

Set( %ServiceAgreements, (
Status => {
‘new’ => { BusinessMinutes => 24 * 60 }, # 1 day
‘open’ => { BusinessMinutes => 48 * 60 }, # 2 days
‘stalled’ => { BusinessMinutes => 72 * 60 }, #3 days
‘followup’ => { BusinessMinutes => 24 * 60 }, #1 day
},
));

Set( %ServiceBusinessHours, (
‘TestHours’ => {
1 => { Name => ‘Monday’, Start => ‘9:00’, End => ‘18:00’ },
2 => { Name => ‘Tuesday’, Start => ‘9:00’, End => ‘18:00’ },
3 => { Name => ‘Wednesday’, Start => ‘9:00’, End => ‘18:00’ },
4 => { Name => ‘Thursday’, Start => ‘9:00’, End => ‘18:00’ },
5 => { Name => ‘Friday’, Start => ‘9:00’, End => ‘18:00’ },
holidays => [qw(01-01 12-25])],
},
));

For example, the first bit of lib/RT/Action/SLA_SetDue.pm shows that $level is being set by the $ticket->SLA method

sub Commit {
my $self = shift;

my $ticket = $self->TicketObj;
my $txn = $self->TransactionObj;
my $level = $ticket->SLA;

And it looks like the Due date is being calculated by a method that takes a few arguments:

my $response_due = $self->Due(
    Ticket => $ticket,
    Level => $level,
    Type => $is_outside? 'Response' : 'KeepInLoop',
    Time => $last_reply->CreatedObj->Unix,
);

This is probably pretty trivial stuff, but could you help me understand how $level is being set, or how I can find the $ticket->SLA method to see how it actually works? The same goes for the $self->Due() method - how can I find this method to see how it works?

I guess I foresee a method for my case being something closer to this:

my $status_due = $self->Due(
     Ticket => $ticket,
     Status => $status,
     Type => 'StatusChange',
     Time => $ticket->CreatedObj->Unix,
 };

Although I’m not sure a Status argument can be substituted in place of the Level argument, and likewise with the Type value. I guess this is more of pseudocode at this point.

Thanks!

You can get an RT config value by using the following method my $agreement = RT::Config->Get('ServiceAgreements');

Let’s trace this a little more to RT::SLA we can see that the Due method just calls the CalculateTime method:

sub Due {
    my $self = shift;
    return $self->CalculateTime( @_ );
}
sub CalculateTime {
    my $self = shift;
    my %args = (@_);
    my $agreement = $args{'Agreement'} || $self->Agreement( @_ );
    return undef unless $agreement and ref $agreement eq 'HASH';
...

So this method accepts a named arg of Agreement or loads what is returned from the Agreement method (Which we would like to use since it has some handy logic).

Once again I haven’t tested any of this but my hope would be that in your new action when you want to get the due date:

my $response_due = $self->Due(
        Ticket => $ticket,
        Level => $level,
        Type => $status,#<-- Changed this
        Time => $last_reply->CreatedObj->Unix,
    );

You can keep the Type key but send the ticket status, and just add statuses as new “Types” in your siteconfg. You may be able to keep your example config but change the top level ‘Status’ back to ‘Levels’. You may need to create a ‘standard’ normal level to have as default for normal SLA I am not sure.

If you want to trace the code some more the methods for SLA are in lib/RT/SLA.pm.

Thanks!

Sorry for the noise but this may just work for you in your custom action have the condition be on status change and have the action be:

sub Commit {
my $self = shift;

my $ticket = $self->TicketObj;
my ($ret, $msg) = $ticket->SetSLA($self->TransactionObj->NewValue);
RT::Logger->error("Failed to update SLA: $msg") unless $ret;
return 1;

}

Hi Craig,

This isn’t noise, this is all great! Ahhh I didn’t see RT::SLA when looking at the other SLA modules - this explains a lot.

So in local/lib/RT/Action/SLA_SetStatusDue.pm I’ve captured the current status in the ticket:

sub Commit {
my $self = shift;

my $ticket = $self->TicketObj;
my $txn = $self->TransactionObj;
my $level = $ticket->SLA;
my $status = $ticket->Status;

And then per your recommendation I have defined Type to pass the $status value

my $status_due = $self->Due(
     Ticket => $ticket,
     Level => $level,
     Type => '$status',
     Time => $ticket->CreatedObj->Unix,
 };

I’m not quite sure how to format the SLA configurations in the config file. Do you think something like this could work?

Set( %ServiceAgreements, (
Levels => {
‘standard’ => {
‘new’ => { BusinessMinutes => 24 * 60 }, # 1 day
‘open’ => { BusinessMinutes => 48 * 60 }, # 2 days
‘stalled’ => { BusinessMinutes => 72 * 60 }, # 3 days
‘followup’ => { BusinessMinutes => 24 * 60 }, # 1 day
BusinessHours => ‘SalesHours’,
},
},
));

Set( %ServiceBusinessHours, (
‘TestHours’ => {
1 => { Name => ‘Monday’, Start => ‘9:00’, End => ‘18:00’ },
2 => { Name => ‘Tuesday’, Start => ‘9:00’, End => ‘18:00’ },
3 => { Name => ‘Wednesday’, Start => ‘9:00’, End => ‘18:00’ },
4 => { Name => ‘Thursday’, Start => ‘9:00’, End => ‘18:00’ },
5 => { Name => ‘Friday’, Start => ‘9:00’, End => ‘18:00’ },
holidays => [qw(01-01 12-25])],
},
));

Finally, regarding your below suggestion, I’m not quite sure how that comes into play

Couldn’t this just use the slightly tweaked built in logic to calculate the due date? For example:

my $due;
 $due = $status_due if defined $status_due

Thanks again for all your help!

If you have this as the action for a scrip that runs On Status Change:

sub Commit {
my $self = shift
my $ticket = $self->TicketObj;
my ($ret, $msg) = $ticket->SetSLA($self->TransactionObj->NewValue);
RT::Logger->error("Failed to update SLA: $msg") unless $ret;
return 1;
}

The SetSLA action will handle setting dates for you and it will reference the RT config %ServiceAgreements just like normal SLA, the only difference is the condition is on status change now.

I’m not quite sure how to format the SLA configurations in the config file. Do you think something like this could work?

Yup this looks good

Hi Craig,

I tested it out, and it’s doing something. Looks like the SLA Level is getting changed entirely on status change, rather than updating the BusinessMinutes for chosen status within that same SLA. For example, I create the ticket with SLA “standard” selected. When I change the status to open, the "SLA changed from ‘standard’ to ‘open’ ". I guess I could configure Due BusinessMinutes for each separate SLA Level, but is this the intention?

Thanks!

Yes, sorry looking closer at your config you should not nest all the statuses inside of ‘standard’.

If I am not mistaken each status can be considered a new SLA level correct?

If that’s the easiest way to make it work, then that’s fine I suppose. What concept I had in mind was to nest them as seen in my prior config. Do I specify Response or Resolve for the action? I really don’t want correspondence to change the Due date, only status change. That’s why I imagined having its own logic entirely, I felt it would be cleaner and not clutter up the SLA’s with many Levels.

However, this appears to work better. The Due date is updated when the status changes.

Set( %ServiceAgreements, (
Levels => {
‘new’ => {
‘Resolve’ => { BusinessMinutes => 8 * 60 }, # 1 day
},
‘open’ => {
‘Resolve’ => { BusinessMinutes => 16 * 60 }, # 2 days
},
‘stalled’ => {
‘Resolve’ => { BusinessMinutes => 24 * 60 }, # 3 days
},
‘followup’ => {
‘Resolve’ => { BusinessMinutes => 24 * 60 }, # 3 days
},
},
QueueDefault => {
‘Test Dev’ => ‘new’,
},
));

Set( %ServiceBusinessHours, (
‘TestHours’ => {
1 => { Name => ‘Monday’, Start => ‘10:00’, End => ‘18:00’ },
2 => { Name => ‘Tuesday’, Start => ‘10:00’, End => ‘18:00’ },
3 => { Name => ‘Wednesday’, Start => ‘10:00’, End => ‘18:00’ },
4 => { Name => ‘Thursday’, Start => ‘10:00’, End => ‘18:00’ },
5 => { Name => ‘Friday’, Start => ‘10:00’, End => ‘18:00’ },
holidays => [qw(01-01 12-25])],
},
));

However, I’m finding that the Due date is updated based on the amount of time from Creation date, rather than status change (or date of the update) date. This is problematic. Should I be using “Respond” instead of “Resolve”?

Thanks!

Hi Craig,

I was able to get the Due date to update relative to the last update of the ticket / on status change. In local/lib/RT/Action/SLA_SetDue.pm instead of:

my $resolve_due = $self->Due(
    Ticket => $ticket,
    Level => $level,
    Type => 'Resolve',
    Time => $ticket->CreatedObj->Unix,
);

I changed Time to:

my $resolve_due = $self->Due(
    Ticket => $ticket,
    Level => $level,
    Type => 'Resolve',
    Time => $ticket->LastUpdatedObj->Unix,
);

This seems to work very well. It doesn’t seem like the most resilient solution, especially if I end up using SLA’s as intended in other queues. Perhaps I should remove the SLA scrips as global defaults, customize this script as something like local/lib/RT/Action/SLA_SetDueOnStatusChange and apply it to just the necessary queue. Better yet, I would incorporate the proper logic in with the existing SLA_SetDue.pm, but I’m not advanced enough to do that.

In the end, and for anyone else interested, this is the logic I’m using to update Due date based on Status with business hours in mind:

Config looks like this:

Set( %ServiceAgreements, (
    Levels => {
            'new' => {
                    'Resolve' => { BusinessMinutes => 8 * 60 }, # 1 day
                    BusinessHours => 'TestHours',
                    },
            'open' => {
                    'Resolve' => { BusinessMinutes => 16 * 60 }, # 2 days
                    BusinessHours => 'TestHours',
                    },
            'stalled' => {
                    'Resolve' => { BusinessMinutes => 24 * 60 }, # 3 days
                    BusinessHours => 'TestHours',
                    },
            'followup' => {
                    'Resolve' => { BusinessMinutes => 24 * 60 }, # 3 days
                    BusinessHours => 'TestHours',
                    },
            },
    QueueDefault => {
            'Test' => 'new',
    },
));

Set( %ServiceBusinessHours, (
    'TestHours' => {
            1 => { Name => 'Monday', Start => '10:00', End => '18:00' },
            2 => { Name => 'Tuesday', Start => '10:00', End => '18:00' },
            3 => { Name => 'Wednesday', Start => '10:00', End => '18:00' },
            4 => { Name => 'Thursday', Start => '10:00', End => '18:00' },
            5 => { Name => 'Friday', Start => '10:00', End => '18:00' },
            holidays => [qw(01-01 12-25])],
            },
));

As mentioned above, 'local/lib/RT/Action/SLA_SetDue.pm was modified on line 104 with:

     Time => $ticket->LastUpdatedObj->Unix,

A custom scrip was written with the following parameters:

Description: Set Due Date on Status Change
Condition: On Status Change
Action: User Defined
Template: Blank

Custom Condition: blank
Custom action preparation code:

return 1;

Custom action commit code:

my $ticket = $self->TicketObj;
my ($ret, $msg) = $ticket->SetSLA($self->TransactionObj->NewValue);
RT::Logger->error("Failed to update SLA: $msg") unless $ret;
return 1;

Thanks a bunch Craig for your assistance!

1 Like

I updated the topic name to better suit the solution described here, and marked appropriate responses as solutions.