How to Automatically Deactivate Users Who Haven't Logged On in 90 Days.

How to Automatically Deactivate Users Who Haven’t Logged On in 90 Days

By

One of the core responsibilities Salesforce Admins handle is user management. You’re creating, updating, and deactivating users while working within a defined number of licenses for your org. Some of you proactively review user login activity and take action accordingly by deactivating users who don’t use or haven’t used Salesforce in a while. Do you run reports every quarter or X number of days to see which users have logged in since, and then go through the manual task or use Data Loader to deactivate those users? What if you didn’t have to lift a finger and Salesforce could do the work for you?

You know me—I’m all for working smart, not hard. With Flow Builder, all of this is possible. Here’s how.

A few lessons you’ll learn by implementing this use case include:

  • How to configure a record-triggered flow and schedule-triggered flow
  • How to use a custom metadata type to manage app configuration or text and avoid hard coding in flows
  • How to reference a custom metadata type without a Get Records element
  • How to test a schedule-triggered flow with a record-triggered flow

Users who have not logged in in over 90 days need to be deactivated

Addison Dogster is the system administrator at Mochi Cupcakes, a fictional cupcake retailer. (Who’s craving a cupcake right now? I want a strawberry shortcake cupcake. Yum!) Part of her job is user maintenance and ensuring that people who use Salesforce have the access they need. She notices that several users in her org have never logged in to Salesforce or haven’t logged in in over 90 days. She cleans up her org by deactivating those users, but she doesn’t want to have to do this every 90 days. Can Salesforce deactivate these users for her? YES!

Use automation to auto-deactivate users

Addison figures she can configure a scheduled flow to run every day that runs through the users. However, she doesn’t want this to run for all users every day. Ideally, the flow would only run for active users whose last login date is over 90 days OR never logged in at all.

Unfortunately, in a schedule flow, you cannot set a relative date on the record filter. Bummer!

Schedule-triggered flow entry conditions does not allow for relative date.

Addison thinks, “I can create a formula on the User object that calculates the days since last login to greater than 90 days.” Nope. The Last Login Date field is not available in formulas. Doh!

Addison knows that in Flow, she can have a Decision to evaluate the length of time since last login and then decide whether to take action.

This auto-deactivation process would only apply to certain profiles, not all, as she would not want to inadvertently deactivate any users associated to the System Administrator or Integration User profiles or any community users. However, Addison does not want to maintain the list of profiles (that is, hard code) in the flow’s record filter. If she needs to add or remove a profile or two, she wants to be able to do this in a pinch WITHOUT having to touch the flow.

She’s going to manage the list of profiles opting into this auto-deactivation via a custom metadata type, where she can add and remove profile records without having to update the flow. That’s working smart, not hard.

Addison also doesn’t want to catch the users off guard and just deactivate their accounts. She’ll notify them and their manager a week in advance that if the user doesn’t log in, they’ll be subject to the deactivation policy and deactivated in a week. This way, if the user still wants access, all they need to do is log in to Salesforce.

Addison’s solution includes:

  • Two custom fields on the User object: one to track the user’s actual profile name, not just the associated profile (profileId) and another to flag the user as qualifying for the auto-deactivation
  • Two custom metadata types: one to contain the profiles for auto-deactivation and another to hold the warning and deactivation thresholds
  • A record-triggered flow to indicate whether a user qualifies for auto-deactivation (through their profile) for any new user, a user whose profile has changed, or a user who is re-activated
  • A schedule-triggered flow that evaluates the last login against the warning and deactivation thresholds and notifies the user and their manager of pending deactivation with continued login inactivity, or deactivates the user account and notifies the user and their manager of the deactivation activity

The solution

Two user custom fields

First, we need access to the user’s profile by profile name, not profileId. To do this, we need a formula: Profile.Name. We’ll use this Profile Name field to compare the user’s profile to the profiles that are subject to the auto-deactivation process.

User custom field to store the name of the user’s profile.

We need a mechanism to track users who are subject to the auto-deactivation process. This is done with a checkbox custom field called “Qualifies for Auto-Deactivation”. Set the default value to unchecked.

User custom field to track user who qualifies for the auto-deactivation process.

Two custom metadata types

Next, Addison configures two custom metadata types to track the profiles subject to the auto-deactivation process and the warning and deactivation thresholds.

Now, some of you are probably wondering, “What is a custom metadata type?” To learn more about this awesome tool to add to your #AwesomeAdmin toolbelt, read Why You Should Avoid Hard Coding and Three Alternative Solutions.

A custom metadata type is like a cousin to the custom object, as the process to create a custom metadata type and custom fields is the same as creating a custom object and its custom fields. Custom metadata types are created in Setup but not in Object Manager. Search “custom metadata types” in the Quick Find in Setup.

First, Addison creates a custom metadata type called “Profiles for Deactivation” with one custom field for Profile Name.

Profiles for Deactivation custom metadata type with custom field for Profile Name.

From the custom metadata type, Addison clicks the Manage Profiles for Deactivation button to add the profiles subject to the auto-deactivation process. Think of these as custom metadata type records, like records for custom objects you create. The difference here is that these records are found only in Setup and not accessed by Salesforce users. Addison creates two custom metadata type records for two profiles. The profile names are entered in the Profile Name field of the custom metadata type record. This must match the profile name exactly or else users with the profile will not fall into the auto-deactivation process.

Profiles for Deactivation custom metadata type with the two custom metadata type records for Sales Associate and Service Rep profiles.

For the second custom metadata type, Addison made this more generic so she can use this same custom metadata type for various needs. This custom metadata type, CustomMetadataType, has a Number, RecordId, and Text fields. For this process, Addison only needs the Number field.

Custom metadata type, CustomMetadataType, with custom fields for Number, RecordId, and Text.

She clicks the Manage CustomMetadataTypes button and creates two custom metadata type records called “DeactivationThresholdInDays” and “WarningThresholdInDays” with the number values 90 and 83, respectively, to track 90 days for the deactivation threshold and 83 days for the warning threshold. Having these as custom metadata type records will be handy a little later on when we test the automation with smaller thresholds. This gives Addison the flexibility to adjust the thresholds without having to update the flow.

Custom metadata type, CustomMetadataType, with the two custom metadata type records for the deactivation and warning threshold values.

A record-triggered flow to qualify/disqualify the user for the auto-deactivation process

Addison creates a record-triggered flow that runs (1) when a new user is created, a user’s profile is modified, or a user is re-activated to set or unset the Qualifies for Auto-Deactivation field in the user record if the user’s profile qualifies for auto-deactivation. The automation uses (2) a Get Records element to retrieve the custom metadata type record that matches the user’s profile and (3) uses an Update Records element to update the Qualifies for Auto-Deactivation field accordingly based on whether the associated custom metadata type for the profile name was found. If the profile is not found, it will uncheck the Qualifies for Auto-Deactivation checkbox so the user will not be picked up for the daily auto-deactivation process Addison will create in the next step.

Record-triggered flow that sets the Qualifies for Auto-Deactivation field on the User record for any new user, user whose profile is updated, or user who is re-activated.

In the Start step, Addison configures the record-triggered flow to run on the User object, when a user record is created or updated if a new user is created, the user’s profile is changed, or the user is re-activated. While we’re updating the User record that triggered the automation, Addison set it to be an after-save flow by selecting Actions and Related Records. By doing this, her flow has access to the user’s profile name, which is not set until after the user record is saved.

Start element of the record-triggered flow to set the Qualifies for Auto-Deactivation field on the User record.

Here’s the formula for the entry condition:

ISNEW() ||
ISCHANGED( {!$Record.ProfileId} ) ||
(ISCHANGED({!$Record.IsActive}) && {!$Record.IsActive})

First, Addison performs a Get Records on the custom metadata type Profiles for Deactivation where the Profile Name field matches the value in the user’s Profile Name field. It stores the first record found and takes the ID and stores it in a variable Addison created called varProfileCMDTRecord.

Get Records element for the Profiles for Deactivation custom metadata type where the value in the Profile Name field matches that of the user’s profile.

Addison created the varProfileCMDTRecord Variable resource to hold the ID of the custom metadata type record that matches the user’s profile. She specifically named the variable using the naming convention varXXXX to indicate that it’s a variable.

Variable to hold the ID of the custom metadata type record if a match is found to the user’s profile name.

Addison uses an Update Records element to update the user record that triggered the flow. Here, she sets the value of the Qualifies for Auto-Deactivation field to a Formula resource: If the varProfileCMDTRecord variable is blank, leave it unchecked; otherwise, check the field.

Update Records element to set the Qualifies for Auto-Deactivation field accordingly.

Addison creates a Formula resource called QualifiesForAutoDeactivationFieldFormula that determines whether to check or uncheck the field based on whether a matching custom metadata type record was found. She uses the XXXXXCFormula naming convention for her Formula resources for consistency.

QualifiesForAutoDeactivationFieldFormula Formula resource.

Here’s the formula used in the QualifiesForAutoDeactivationFieldFormula Formula resource:

If (ISBLANK({!varProfileCMDTRecord}), false, true)

Addison debugs her record-triggered flow to ensure it works as expected before activating it.

Note: Prior to implementing this new auto-deactivation process, you need to perform a one-time mass update of the Qualifies for Auto-Deactivation field for all your qualified users.

A schedule-triggered flow to handle the auto-deactivation process

Now that the Qualifies for Auto-Deactivation field is set on the User record, Addison creates a schedule-triggered flow to handle the auto-deactivation process warning users that they are on the verge of being deactivated or, if they have not logged in in over 90 days, to deactivate the users.

Here’s the automation in a nutshell. (1) The schedule-triggered flow is set to run daily at a specific time, with a starting date in the future if the user is active and the Qualifies for Auto-Deactivation is true. (2) A Decision element is used to determine whether the login threshold has been exceeded. (3) If the threshold is exceeded, a Get Records element is used to get the manager’s email address, (4) an Assignment element is used to add the user and manager’s email addresses to a collection, (5) a Decision element is used to determine whether the deactivation threshold is met or if a warning should be issued. If it’s determined only the warning threshold is exceeded, (6) a Send Email Action is used to notify the user and manager of the upcoming deactivation unless the user logs in to Salesforce in the next 7 days. If the deactivation threshold is met, then (7) a Send Email Action is used to notify the user and manager that the user is deactivated per the login policy, and (8) the Update Records element is used to deactivate the user.

Schedule-triggered flow to either warn or deactivate the user if either threshold is met.

First, Addison configures the Start element to run on a Daily Frequency with a start date in the future. In this example, Addison set the start date to December 8, 2023, at 12:30 AM daily.

Start element for the scheduled flow to run daily at 12:30 AM starting December 8, 2023.

Addison wants to be selective as to which records will run through this automation, so she configures the flow to run on records in the User object but only if the user record is active AND the Qualifies for Auto-Deactivation field is checked. If the user record does not meet the entry condition, it is skipped.

Configured object and entry filter conditions in the Start element.

Addison uses the Decision element to determine whether the threshold has been exceeded. For the exceeded outcome, she configures it to meet these conditions: 1 or (2 and 3).

  1. The DaysSinceLoginFormula Formula resource evaluates to greater than or equal to the WarningThresholdInDaysFormula Formula resource.
  2. The DaysSinceCreationFormula Formula resource evaluates to greater than or equal to the WarningThresholdInDaysFormula Formula resource.
  3. The user’s last login date has no value (that is, the user has never logged in to Salesforce since user creation).

The default outcome is Not Exceeded.

Configured Decision element to determine whether threshold is exceeded.

Let’s do a deeper dive into the Formula resources referenced in the Decision element.

The DaysSinceLoginFormula Formula resource determines the number of days since the user last logged in to Salesforce with the Number data type and zero decimal places.

Today()-DATEVALUE({!$Record.LastLoginDate})

DaysSinceLoginFormula Formula resource.
The DaysSinceCreationFormula Formula resource determines the number of days since the user was created using the Number data type with zero decimal places.

Today()-DATEVALUE({!$Record.CreatedDate})

DaysSinceCreationFormula Formula resource.
The WarningThresholdInDaysFormula Formula resource pulls in the value from the Number field of the WarningThresholdInDays custom metadata type record without requiring a Get Records element. (Shoutout to Andy Utkan’s blog Using Custom Metadata Types to Control Record-Triggered Flows Without Get for sharing this hidden gem!)

Note: To get the correct syntax for the custom metadata type record field, Addison created a validation rule to access the syntax for the custom metadata type record field. From there, she copied and pasted the syntax into the Formula resource since Flow doesn’t currently provide the ability to navigate to it within the Formula resource.

{!$CustomMetadata.CustomMetadataType__mdt.WarningThresholdInDays.Number__c}

WarningThresholdInDaysFormula Formula resource.
Next, Addison uses a Get Records element on the User object to find the manager’s user record (that is, userId is the user’s manager ID).

Get Records element to get the user’s manager user record information.

Addison will use the out-of-the-box Send Email action later in her process to send emails to the user and their manager. She needs to get the email addresses into a collection variable for use in the Send Email action. To accomplish this, Addison uses an Assignment element to add the user and manager’s email addresses to a varEmailCollection Variable resource using the Add operator.

Assignment element to add user and manager’s emails to a collection variable.

Addison creates a collection variable called varEmailCollection. A collection variable is a variable that holds multiple values—in this case, the user and manager’s email addresses. A collection variable will store the email addresses as a string.

varEmailCollection collection variable.

Next, Addison uses another Decision element to determine whether to go down the Warning or Deactivation path. She configures the Warning outcome to meet: 1 or (2 AND 3).

  1. DaysSinceLoginFormula Formula resource evaluates to less than the DeactivationThresholdInDaysFormula Formula resource.
  2. DaysSinceCreationFormula Formula resource evaluates to less than the DeactivationThresholdInDaysFormula Formula resource.
  3. The user’s last login date has no value (that is, user has never logged in to Salesforce).

The default outcome is Deactivation.

Configured Decision element to determine Warning or Deactivation path.

Let’s do a deeper dive into the new DeactivationThresholdInDaysFormula Formula resource.

The DeactivationThresholdInDaysFormula Formula resource pulls in the value from the Number field of the DeactivationThresholdInDays custom metadata type record without requiring a Get Records element.

{!$CustomMetadata.CustomMetadataType__mdt.DeactivationThresholdInDays.Number__c}

 DeactivationThresholdInDaysFormula Formula resource.

Following the Warning path, Addison uses the Send Email action to notify the user and their manager that if they don’t log in to Salesforce in the next 7 days, the user will be deactivated.

She configures to use (1) the WarningEmailTextTemplate Text Template resource as the Body and (2) the varEmailCollection Variable resource as the Recipient Address Collection, then (3) sets the Rich-Text-FormattedBody to true, (4) sets the Subject to Warning: Your Salesforce Account May Be Deactivated Soon, and (5) sets Use Line Breaks to true.

Configured Send Email Action to send a warning notification to the user and manager.

Let’s take a closer look at the WarningEmailTextTemplate Text Template resource. Addison follows a specific naming convention for her Text Template resources: XXXXXTextTemplate. Using the Text Template resource, Addison can control the formatting of her email body and use merge fields to pull data dynamically into her email. In this example, Addison uses the user’s first name.

WarningEmailTextTemplate Text Template resource.

Following the Deactivation path, Addison uses the Send Email action to notify the user and their manager that the user is deactivated.

She configures to use (1) the DeactivatedEmailTextTemplate Text Template resource as the Body and (2) the varEmailCollection Variable resource as the Recipient Address Collection, then (3) sets the Rich-Text-FormattedBody to true, (4) sets the Subject to Your Salesforce Account Has Been Deactivated, and (5) sets Use Line Breaks to true.

Configured Send Email Action to send the deactivation notification to the user and their manager.

Let’s take a closer look at the DeactivationEmailTextTemplate Text Template resource used to hold the body of the email. Similar to the WarningEmailTextTemplate Text Template resource, this too uses the merge field for the user’s first name.

DeactivationEmailTextTemplate Text Template resource.

Addison uses the Update Records element in the last step of the Deactivation path that will deactivate the user record where she uses the $Record global variable (this is the user record that triggered the flow), and sets the Active field to unchecked.

Update Records element to deactivate the user.

Thoroughly test your scheduled flow as a record-triggered flow

If you’ve ever tried to debug a schedule-triggered flow, you know it finds a record that meets the conditions and executes the flow. You can’t specify a test record to use to test various paths. You can save the schedule-triggered flow as a record-triggered flow with a few minor tweaks to test your automation before you activate and deploy it for use in production. Production is not the place to find out your automation did not work as intended.

Do a Save As and select A New Flow. Addison gives it the same name as the schedule-triggered flow but adds “TEST” at the end. Before she saves, she updates the type from Schedule-Triggered Flow to Record-Triggered Flow.

Shows saving a new flow as a record-triggered flow.

There are a few tweaks you need to make to ensure it references the user record correctly, but this is worth it since you’re then able to use this to test your schedule-triggered flow.

In the Start element, (1) select User as the object, and set the flow to trigger (2) when a record is updated.

Configure the Start element.

Save the flow.

Before Addison uses the record-triggered flow to test, she goes to the CustomMetadataType custom metadata type, clicks Manage CustomMetadataType records, and edits the Number field for the WarningThresholdInDays and DeactivationThresholdInDays records from 83 and 90 to 1 and 2, respectively. This will bring down the warning and deactivation threshold to 1 and 2 days, so Addison can test this using test users in a sandbox that have not logged in in over 2 days and logged in for 1 day.

CustomMetadataTypes custom metadata type list view.

Addison updates the Number field for the WarningThresholdInDays custom metadata type record from 83 to 1.

WarningThresholdInDays custom metadata type record.

Addison updates the Number field for the DeactivationThresholdInDays custom metadata type record from 90 to 2.

DeactivationThresholdInDays custom metadata type record.

Now, Addison debugs the flow by selecting a user record that hasn’t logged in to Salesforce in over 2 days. If the flow executes as expected, the blue path will follow like the image shown below, deactivating the user.

Successful debug of a user who has not logged in to Salesforce in over 2 days; Deactivated path.

Addison now debugs the flow by selecting a user record that has not logged in to Salesforce in over 1 day. If the flow executes as expected, the blue path will follow like the image shown below, warning the user.

Successful debug of a user who has not logged in to Salesforce in 1 day; user meets the warning threshold.

Lastly, Addison performs negative testing and debugs it using a user who has logged in today. If the flow executes as expected, the blue path will follow the image shown below.

Successful debug of a user who has logged in to Salesforce today; user has not exceeded the threshold.

Once Addison completes her testing, she updates the WarningThresholdInDays and DeactivationThresholdInDays custom metadata type records from 1 and 2 to 83 and 90, respectively.

Try this at home, I mean, work

Build this in a sandbox and give it a whirl. Then, once you’ve successfully tested this, deploy all the components to production, sit back, and have Salesforce do the warning and deactivation notifications and actions for you!

Resources

Overcome access dilemmas with permission sets

Use Permission Sets To Overcome Common Access Dilemmas

As an Awesome Admin, it’s probably in your nature to look for any way to optimize a process or situation! As part of that never-ending desire for optimization, I would bet that you’ve spent a lot of time thinking about your permissions setup in Salesforce. Salesforce provides multiple ways to grant permissions to users, each […]

READ MORE
Advance Your Admin Career With Dev Fundamentals

Advance Your Admin Career With Dev Fundamentals

Ready to take the next step in your admin career but unsure where to start? Take a page out of my book and learn development fundamentals to jumpstart your abilities as an advanced admin and extend your Salesforce Platform knowledge. Several years ago, I was at a career tipping point. I felt solid in my […]

READ MORE