At times there is a need to calculate business hours spent on certain things in Salesforce. In following guide I’ll explain how you can get the difference between two date/time fields relatively to your work hours and holiday schedule.
Calculate Business Hours by Using Formula Fields
Let’s start from the basics. For a simple calculation between two dates you could use the following:
Formula above will return the number difference in days.
If you need to calculate the difference between two date/time fields you could use this:
If you need to calculate business hours between two date/time fields the Salesforce formula becomes more complex.
Here is how it looks:
The good thing is that it’s easy to implement. You just need to replace ‘date/time‘ fields with yours.
All the above formulas are explained in developer documentation that can be found here. Please take a moment to read and get familiar.
However, there are disadvantages to using such approach. The last formula does not take into consideration holidays, time zones, different work hours (in case you have multiple locations).
Luckily there is a way around it. It is more complex, however, the results are more accurate. Instead of the formula field, we’ll use Apex Trigger. The BusinessHours Class should take care of all calculations. We just need to ensure that the data is valid.
For more info on BusinessHours class please click here.
First, let’s add business hours to Salesforce. To do so, you need to go Setup -> Start typing ‘business’ in search bar.

Go ahead a click New Business Hours button. New window will open for you to specify the name, time zone and work hours for each day.
For our example, let’s add one Business Hours Record and call it ‘Main Office‘. Next we need to add holidays. To do so, go to Setup -> Type in search bar ‘Holidays‘.
Click ‘New’ button to add all necessary holidays. For each holiday there is an option to set it as recurring holiday. All the rest holidays that have different date each year need to be added separately.
The last step here is to assign holidays to business hours. You just need to go back to business hours record that you’ve created earlier and add holidays. Go ahead and click Add/Remove button under Holidays related list to assign holidays to business hours.
Calculate Business Hours by Using Trigger (Single Location)
Once all set, we are ready to move forward with our trigger. Let’s say we need to know how much time is spent on a case during business hours.
We’ll use the following fields:
- Date/Time Open (API – CreatedDate)
- Date/Time Closed (API – ClosedDate)
- Elapsed Time (API – Elapsed_Time__c)
For this example, our trigger will work on before update event type. At first, we need to select our default business hours record. If it has been found, it’s safe to proceed with the trigger.
Since Case Date/Time Opened field is actually CreatedDate, there is no need to validate if it’s empty. The date will be auto-generated when the case is created. We just need to make sure that Date/Time Closed (API – ClosedDate) is not empty and is updated. We don’t want a trigger to fire for other updates to a case.
Next, we’ll use BusinessHours Class to calculate time. The end result will be saved in the Elapsed Time custom field. Since the trigger is set on ‘before update‘, there is no need to perform DML operation.
Trigger with comments is listed below:
Calculate Business Hours by Using Trigger (Multiple Locations)
Let’s take another example with multiple business hours. This is helpful if you have different locations. Also, let’s say we want to calculate business hours on update & insert .
The ‘Case’ object will not be best for this example, so let’s just use a custom object. Each record should be tied to a specific location. That way it will be possible to calculate business hours for each place. If you use the same locations for other objects, it is wise to use a lookup that points to a location record itself. If not, the location field can be a simple picklist.
- Object name: Custom Object (API Custom_Object__c);
- Date/Time field: Start Time (API Start_Time__c);
- Date/Time field: Completed Time (API Completed_Time__c);
- Date/Time field: Hours Spent (Hours_Spent__c);
- Picklist field: Location (API Location__c) OR Formula Text field: (API Location__c). A formula should return location name as text. For example: Location__r.Name
Let’s also add business hours for different locations. For example: Chicago, New York, Lost Angeles. Please note that Location names should match Business Hours names.
The trigger itself will be very similar to the previous version. The core logic stays the same, we just add separate rules for insert and update operations. Also, we add Map to hold all of our business hour records. The key for this map will be the location name. As you remember, we name business hours the same as locations. That way we can easily cross-reference location to business hours record and do the math.
If its insert, we want to make sure that Start Time and Completed Time fields are not empty. Because it will not make sense to do the calculation. For update operation, we also check if fields are not NULL and if they were changed.
Please take a look at trigger below. Comments should help you out with understanding the flow.
The trigger is ready to go. You just need to replace object and field APIs with your’s. I’m hoping you’ve learned something new today. Now you should be able to calculate number of business hours spent between two date/time fields in Salesforce. Please let me know if there is an easier way to do this. Feel free to post your suggestions!
Test Class Update:
Here is a very basic test class that I’ve put together. As is it will cover about 57% of the code.
The reason is that ClosedDate on the Case object won’t populate when changing the Status field on ‘before update‘ trigger. The simplest workaround would be to set a trigger to work on ‘after update‘. But in this case, you’ll have to consume SOQL query to get the case record & perform DML (update operation).
Feel free to comment if you have any suggestions!




Hi Oleksander,
Thank you so much for this it’s very helpful! I’m an admin trying to write code to do this. I’m creating a Trigger with your code but I need to modify for just 2 date values on Lead object. I’m getting an error on the last section.
I’ve updated to: updateList.add(leadObj)
Error: ‘Variable does not exist: updateList’
Let me know if you have any ideas for me! Thank you.
Claire
Hi Claire! You are very welcome! I’m glad I could help. Please take a look at line 3 from my example for a single location. I’ve updated the code. You want to make sure to initialize an empty list for the same object you are updating for future updates. Please let me know if it makes sense.
Thank you! Very helpful. I’m SO sorry now because I actually ended up using your multi location one instead because we have some users in the UK that I realized I needed to account for. But I super appreciate your quick response and modification.
But your code was amazing I was able to tweak to match my fields and it is working perfectly! Just have to figure out how to write a test class for it now :)
I bookmarked your website this page has been so helpful for me.
I’m glad it worked for you! I just looked at the code one more time. The line where it says updateList.add(leadObj) is actually not necessary. And the list that I’ve suggested earlier as well. The reason is because trigger is firing on ‘before’ condition. Which means you don’t have to perform DML (Save) operation at the end. Read more: https://www.forcetalks.com/salesforce-topic/why-we-cant-use-dml-before-trigger-in-salesforce/
You are the best…. Thanks mate
Hi Oleksandr! Thank you so much for this. This is exactly what I’ve been looking for to calculate “Minutes to First Response” for cases. I’ve got this working great in sandbox, but when I try to promote it Prod, it says I need at least 1% test coverage. I’m not a dev and am a bit lost. Do you have an example test class for the “Single Location” example? Thank you!
Hi Kerry! I’ve updated the article with a very basic test class. It’ll get you about 57% of code coverage.
You may notice that in test class I’m just inserting and updating a case record. This is because all date fields are actually system fields and can’t be simply modified. It will take a more complex approach to adjust system fields. Let me know if you have questions!
Thank you so much!! Really appreciate the fast response!
Oh my goodness….I’m working on Minutes to First Response and I’m not being successful at all!
I took this snippet and just replace ClosedDate with the date I’m capture first response in….but it’s not writing to the new Time to First Response. Not sure what I am doing incorrectly.
Hi Velma! I believe there can be multiple reasons. I’d recommend using standard fields first to confirm that the trigger works. Like created date and last modified date.
Then try replacing standard fields with your custom ones.
Thank you Oleksandr. I’ve got that working and have combined my triggers….I’m now struggling with combining triggers. I have something very similar but it uses the LastModifiedDate and needs to go in the after along with the Salesforce Labs Unmanaged package for CaseStatusUpdateTriggerHandler and I’m struggling with doing this effectively. I think I have said “I’m not a developer” 1000 times!
Hi Velma, great to hear you are making progress. Generally, the best practice is that you don’t have any logic in triggers. Apex triggers should be calling apex functions. Good luck!
Hi Oleksander,
I’ve used the code ROUND( 8 * … , 0 ) and replaced date/time_1 and date/time_2 with custom fields. In my case I want to subtract date/time_1 from date/time_2 like you do in the example.
Without having made any adjustments, I’m getting strange results. For example:
date/time_1 is 22-4-2021 16:00 and date/time_2 is 22-4-2021 15:00. So 16:00 – 15:00
I expect 1 as a result, but receive zero. I tried to break down the formula to understand better, but yikes :/ really complex for me.
Can you help me further?
Thanks in advance :)
Hi Jagmohan! I believe you are receiving zero because your formula filed has no decimals. If you were to add two decimals to it you’d see the result of 0.04. Since you don’t have decimals, Salesforce will round down to the nearest number – 0. Now, the result of 0.04 is the difference in days. If you want to convert it into hours you need to multiply it by 24. Because one day is 24 hours. Your formula will look like this: (DateTime1 – DateTime2) * 24 If you want to get the result in minutes your formula will be:… Read more »
Hey Oleksandr,
Thanks for the quick reply. I deleted the ROUND() function from the formula but still I receive a 0 as result. Even if I increase the decimals in the formula field Luckily we have a developer in our team. Hope he can get it to work with the BusinessHours Class trigger.
Hi Oleksander,
I’m trying to do this on the lead object to calculate the time from when a lead is created until the first activity is logged against them. I have a field which gets time stamped with the date the Lead was first contacted. How could I go about setting that up with his example? The field which gets time stamped is called First_Activity_Date_Time__c
Hi Ryan! You can just simply use the code from this blog. Replace custom object API with Lead object API. And then use your fields in the trigger.
In your case, you don’t need to use a part of the trigger that fires on Insert. You need to use the part that fires on update. This is because your First_Activity_Date_Time__c is updated after lead is inserted.
Thanks for this, just wanted to point our a typo. You have a comment that says 60*60*100, but it should say 60*60*1000, thanks! -Sean
Hi Oleksandr, this is really great, thank you. I’m putting this into my Sandbox now for the purpose of measuring the time between Lead creation and the date that it moved to the 2nd Lead Status (separately populated with a Flow). When I test this with a test Lead and move the lead to the 2nd lead status, I get this error message: “You encountered some errors when trying to save this record There’s a problem saving this record. You might not have permission to edit it, or it might have been deleted or archived. Contact your administrator for help”… Read more »
Hi Jack. Try changing your trigger to ‘before update’.
When you run on ‘after update’ the triggering record becomes read-only. If you wanted to update this record on ‘after update’, you need to do a SOQL query to get this record first. Like: SELECT ID FROM Lead where Id IN: trigger.new. And then all the rest of your stuff + DML operation at the end of the trigger.
But overall as a rule of thumb, the same record updates should run with ‘before update’ triggers.
Hi Oleksander,
Good Day to you!
Is there a way to calculate business hours in realtime without using a hourly scheduled update?
We wanted to track the elapsed time where the records were not getting touched by the approver given the status assigned for them.
Thanks!
Hi Mark! I don’t think real time is possible if you wanted to run reports and see live data. The closest you can get to real-time with help of out of the box tools will be by using formula fields mentioned above. But they are not as accurate as business hours apex approach. From the single record view you might be able to setup some kind of LWC that calls business hours method on page load & every second moving forward to give you ‘real-time’ data. But this will not write to a field. Only for display. This might be… Read more »
I have tried to use it on the Task object but it doesn’t seem to work correctly.
Could you help me?
trigger TestDateDaysTask on Task (before update, after update) {
BusinessHours defaultBH = [SELECT Id FROM BusinessHours WHERE IsDefault = true Limit 1];
if(defaultBH != NULL){
for(Task taskObj : trigger.new ){
System.debug(‘Aqui si llego’);
if(taskObj.IsClosed == true && taskObj.CompletedDateTime != NULL){
if(taskObj.IsClosed == true && taskObj.CompletedDateTime != NULL && Trigger.newMap.get(taskObj.Id).CompletedDateTime != taskObj.CompletedDateTime){
System.debug(‘Aqui también’);
decimal result = BusinessHours.diff(defaultBH.Id, taskObj.CreatedDate, taskObj.CompletedDateTime);
decimal resultingHours = result/(60*60*1000);
System.debug(resultingHours.setScale(1));
taskObj.CC_TiempoTranscurridoTrigger__c = resultingHours.setScale(1);
}
}
}
}
HI Cesar, please be more specific with your issue. I’m not sure what you mean by ‘doesn’t seem to work correctly’. One thing that I’d suggest is to avoid using nested IF statements with redundant conditions such as:
if(taskObj.IsClosed == true && taskObj.CompletedDateTime != NULL){ }You’ve probably resolved the issue by now, but let me know if you still have open questions.
I’m terrible at Apex but what you’ve created seems to be exactly what I am looking for. Is there any chance that logic is possible in an invocable apex action for a flow?
Hi Greg, yes it is possible. You just pass the record from a FLOW to Invocable apex.
Notice how in the example I iterate over Trigger.New. In your invocable method you need to iterate over the collection that you pass from the flow instead of Trigger.New. And then at the very end you’ll need to update that collection.
I don’t have an update in my example because my trigger runs in ‘before update’ context.