The question sounds simple: how many hours did cases spend in New, in Working, and in Waiting on customer last month, and which team held them longest. Salesforce can tell you every time a status changed. The duration is the gap between one change and the next, and that gap is the part you have to build.
This guide uses Case and its Status field. The same steps work for Lead status, Opportunity stage, or a picklist on a custom object.
What standard Salesforce gives you
Field history tracking
In Setup, under Field History Tracking, you turn on history for an object and choose its fields: up to 20 standard and custom fields per object. Each change to a tracked field adds an entry to the record's History related list with the date, time, nature of the change, and who made it. Salesforce stores the entries in a history object, such as CaseHistory, whose rows carry the field name, the old value, and the new value.
A row records a moment. Nothing in it says how long the old value lasted, so a history report lists changes, not stays. To get time in a status from history you subtract each row's date from the next row's date for the same case, outside the report or in code.
Opportunity stage duration
Opportunities are the exception. The Opportunity History report type has From Stage and To Stage columns, and opportunity reports offer a Stage Duration column: the number of days the opportunity was in the stage. Two limits matter. It counts days, and when an opportunity returns to a stage, Salesforce's own example shows the report keeping only the first visit: 2 days in Proposal, then Negotiation, then 3 more days in Proposal reports 2 days, not 5. And it covers stage on opportunities only, not owner, and not other objects.
Case Age
Case reports include an Age column. For an open case it is the time from creation to now; for a closed case, from creation to closing. You can show it in days, hours, or minutes, and it does not take into account holidays in the case's business hours. You can also enable a Business Hours Age column, which ages the case by your business hours instead of calendar days; it appears on the standard case reports, not on custom report types built on Case.
Both measure the whole life of the case. Neither says how much of that time was spent in Waiting on customer.
Build it yourself with a record-triggered Flow
The do-it-yourself version stamps the time of each status change on the case and, on the next change, adds the minutes since that stamp to a field for the status the case just left. It needs no code and no app.
Fields on Case
| Field | Type | Holds |
|---|---|---|
Status_ | Date/Time | When the current status began |
Minutes_ | Number | Total minutes spent in New |
Minutes_ | Number | Total minutes spent in Working |
Minutes_ | Number | Total minutes spent in Waiting on customer |
Add one Number field for every Status value you report on.
The Flow
- Create a record-triggered Flow on Case that runs when a record is updated, optimized for Fast Field Updates. This kind of flow runs before the save, and changes it makes to
{!$Record}are saved with the record, so no Update Records element is needed. - Set the entry condition to Status Is Changed True, and run the flow every time a record is updated and meets the condition. Is Changed is always false when a record is created, which is why the formula below falls back to the case's created date.
- Add a formula resource,
MinutesInStay, of type Number with two decimal places:
Subtracting one Date/Time from another returns days as a decimal, so multiplying by 1,440 gives minutes.( {!$Flow.CurrentDateTime} - BLANKVALUE({!$Record.Status_Changed_At__c}, {!$Record.CreatedDate}) ) * 1440$Flow.CurrentDateTimeis the moment the flow runs the element. - Add a Decision on
{!$Record__Prior.Status}, the value the case had just before this save, with one outcome per status: Equals New, Equals Working, and so on. - In each outcome, an Assignment sets that status's field to a formula, so a blank field counts as zero. For New:
That is one formula per status.BLANKVALUE({!$Record.Minutes_in_New__c}, 0) + {!MinutesInStay} - After the Decision, one Assignment sets
{!$Record.Status_Changed_At__c}to{!$Flow.CurrentDateTime}.
Cases that already exist have no stamp, so their first stay would count from the day the case was created. Before you activate the flow, set Status_Changed_At__c on open cases to the activation time with a data import, and treat that first stay as partial.
One row per stay instead
Fields on the case answer "how long did this case spend in each status". To average by status across a team in one report, store each stay as a row: a custom object such as Case Status Stay, with a lookup to Case, the status, entered and left date/times, and minutes. A Fast Field Updates flow can only update the triggering record, so the row comes from a second record-triggered flow optimized for Actions and Related Records, which runs after the save.
Where it breaks
- Calendar time only. The formula subtracts clock time, so a case that waits from Friday evening to Monday morning counts the weekend. The formula function reference lists no function that reads your org's business hours, and Salesforce's own sample business-hours formula assumes a fixed 9 to 5 day in one time zone, and does not account for daylight saving time, holidays, or your configured business hours. Business hours need Apex: the
BusinessHours.diffmethod returns the milliseconds between two date/times for a set of business hours, and an@InvocableMethodmakes that callable from the flow. - No history before the day you build. The flow sees only the changes made after it is active. Field history works the same way: Salesforce tracks from the moment you turn it on and records nothing earlier.
- A field per status, per object. A new status value needs a new field, a new outcome, and a new formula. Lead and Opportunity each need their own fields and their own flow.
- Owner time is a second build. Time with each owner, user or queue, needs its own stamp, its own fields or rows, and its own flow, and it cannot answer "how long did Tier 2 hold cases that were waiting on the customer" unless the two builds share rows.
- One report per object. Case minutes live on Case and Lead minutes on Lead, so there is no single list of stays to report on across objects.
- History has limits too. Field history covers up to 20 fields per object. Salesforce keeps it for up to 18 months, and up to 24 months through the API. Field Audit Trail, sold with Salesforce Shield or as Field Audit Trail licenses, keeps history until you delete it and tracks up to 200 fields per object, with access through the API only.
None of this makes the Flow wrong. For one object, one status field, and a team that works around the clock, it is a good answer. Each item above is where admins usually end up adding fields, flows, or Apex.
What Sojourn does instead
Sojourn is a managed package that records each stay as a row, an Interval, with the value, the owner, and business and calendar minutes on every row. It does this today for status-style picklists and Owner on Case, Lead, Opportunity, and any custom object, with business hours set per org, object, field, or queue.
Release 1 adds Backfill from your existing field history, Goals and breach flags, Live values refreshed every 15 minutes, and 20 report templates and 4 dashboards. Release 1 is in development, and early access is open.
See your own cases in business hours.
Install Sojourn in a sandbox, choose what to track, and see the first Intervals the same day.
Sources
Every Salesforce fact on this page comes from these official pages.
- Field History Tracking Overview
- Track Object Field History
- CaseHistory object reference
- Opportunity History Report
- Tips for Working with Opportunity Reports
- Create Salesforce Report on Duration of Opportunity Stage
- Fields Available for Case Reports
- Enabling Business Hours Age in Case Report
- Before-Save Record-Triggered Flows
- Record-Triggered Flow Considerations
- How Entry Conditions Work in Record-Triggered Flows
- Flow Operators in Decision, Wait, and Collection Filter Elements
- Global Variables Resource ($Record and $Record__Prior)
- $Flow Global Variables Resource
- Flow Operators in Assignment Elements
- Using Date, Date/Time, and Time Values in Formulas
- Formula Operators and Functions by Context
- Sample Date Formulas
- BusinessHours Class
- InvocableMethod Annotation
- Field Audit Trail