# Inactive Account Day Threshold Handling Using Transforms

**URL:** <https://developer.sailpoint.com/discuss/t/inactive-account-day-threshold-handling-using-transforms/216210>\
**Category:** Community Blog\
**Tags:** sailpoint-identity-security\
**Created:** [July 11, 2026, 2:39am UTC](https://developer.sailpoint.com/discuss/t/inactive-account-day-threshold-handling-using-transforms/216210 "2026-07-11T02:39:00Z")\
**Posts on this page:** 1\
**Page:** 1

<div class="post-metadata">

**Author:** ![jsosa](https://sea1.discourse-cdn.com/sailpoint/discuss/user_avatar/developer.sailpoint.com/jsosa/32/31578_2.png) [@jsosa](https://developer.sailpoint.com/discuss/u/jsosa)\
**Post date:** [July 11, 2026, 2:39am UTC](https://developer.sailpoint.com/discuss/t/inactive-account-day-threshold-handling-using-transforms/216210/1 "2026-07-11T02:39:00Z")

</div>

![0db1ba04-df46-4246-9b73-7e0ee832c0b0](https://global.discourse-cdn.com/sailpoint/original/3X/0/c/0c52a7de1e1c9db45a64d1383239cda483621d10.jpeg)

# **Background**

A common request from our clients is to monitor some last-login attribute — the field that records the date and time a user last signed in, such as **lastSignInDateTime** on an Entra ID account or **lastLogonTimestamp** on Active Directory — and act based on how many days remain until a given expiration. While workflows do let us compare dates, processing thousands of identities every day and comparing them one by one results in an enormous, and possibly unfeasible, compute cost. Let’s explore how to do this more efficiently using a Transform.

# **Challenge**

We were asked to monitor the last-login attribute of the Entra ID account. The identity must be disabled once 45 days of inactivity have passed. In addition, different notifications must be sent when 1, 3, and 7 days remain before reaching the time limit.

# **Solution**

The solution is fairly simple. Instead of evaluating the last-login date of every identity each day, we only want to act on the ones that have crossed a meaningful threshold: 38, 42, 44, or 45 days of inactivity. To do this, we classify each identity into a single status value based on how long it has been inactive:

- **ACTIVE** — 37 days of inactivity or fewer
- **NOTIFY\_7** — once it passes 38 days (7 days before expiration)
- **NOTIFY\_3** — once it passes 42 days (3 days before expiration)
- **NOTIFY\_1** — once it passes 44 days (1 day before expiration)
- **EXPIRED** — once it reaches the 45-day maximum

Because this status only changes when an identity crosses one of these boundaries, we no longer scan thousands of identities every day — we simply react when an identity moves from one status to the next.

We’ll use a static transform that defines the following variables:

- **d45** — returns **true** if the login date is on or before today minus 45 days
- **d44** — returns **true** if the login date is on or before today minus 44 days
- **d42** — returns **true** if the login date is on or before today minus 42 days
- **d38** — returns **true** if the login date is on or before today minus 38 days

The result is then resolved with a simple cascade:

IF (d45 == true) → EXPIRED

ELSE IF (d44 == true) → NOTIFY\_1

ELSE IF (d42 == true) → NOTIFY\_3

ELSE IF (d38 == true) → NOTIFY\_7

ELSE → ACTIVE

The order of evaluation is critical here. An account that has passed 44 days of inactivity has, by definition, also passed 42 and 38 — so several of these variables will be **true** at the same time. By checking from the longest threshold to the shortest, the first match wins and correctly determines the real inactivity status of the account. It’s also worth noting that the **ELSE** branch can only be reached when the account has fewer than 38 days of inactivity (37 or fewer) — in other words, a still-active account.

With this analysis in place, let’s build our transform step by step. First, we’ll define each **d#** variable as a **Date Compare** transform, where the first value is the last-login attribute and the second is the current date minus the corresponding number of days ( **#** ):

```json
"d45": {
            "type": "dateCompare",
            "attributes": {
                "firstDate": {
                    "type": "firstValid",
                    "attributes": {
                        "values": [
                            {
                                "attributes": {
                                    "sourceName": "Entra ID",
                                    "attributeName": "lastSignInDateTime"
                                },
                                "type": "accountAttribute"
                            },
                            {
                                "type": "dateFormat",
                                "attributes": {
                                    "input": {
                                        "type": "dateMath",
                                        "attributes": {
                                            "expression": "now"
                                        }
                                    },
                                    "inputFormat": "yyyy-MM-dd'T'HH:mm",
                                    "outputFormat": "ISO8601"
                                }
                            }
                        ]
                    }
                },
                "secondDate": {
                    "type": "dateFormat",
                    "attributes": {
                        "input": {
                            "type": "dateMath",
                            "attributes": {
                                "expression": "now-45d"
                            }
                        },
                        "inputFormat": "yyyy-MM-dd'T'HH:mm",
                        "outputFormat": "ISO8601"
                    }
                },
                "operator": "lte",
                "positiveCondition": "true",
                "negativeCondition": "false"
            }
        }

```

Note that the first value is a **First Valid** transform, which ensures we don’t inadvertently invalidate identities that, for whatever reason, don’t have an Entra ID account. In those cases, we simply assign today’s date as the value — as if the identity had just used the account — which will always resolve to an active status:

```json
"firstDate": {
    "type": "firstValid",
    "attributes": {
        "values": [
            {
                "attributes": {
                    "sourceName": "Entra ID",
                    "attributeName": "lastSignInDateTime"
                },
                "type": "accountAttribute"
            },
            {
                "type": "dateFormat",
                "attributes": {
                    "input": {
                        "type": "dateMath",
                        "attributes": {
                            "expression": "now"
                        }
                    },
                    "inputFormat": "yyyy-MM-dd'T'HH:mm",
                    "outputFormat": "ISO8601"
                }
            }
        ]
    }
}

```

To build the other variables, the inner Date Compare value is exactly the same — the only thing that changes is the number of days subtracted from **now**. For example, for **d44** it will be **now-44d** :

```json
"d44": {
    "type": "dateCompare",
    "attributes": {
        "firstDate": {
            "type": "firstValid",
            "attributes": {
                "values": [
                    {
                        "attributes": {
                            "sourceName": "Entra ID",
                            "attributeName": "lastSignInDateTime"
                        },
                        "type": "accountAttribute"
                    },
                    {
                        "type": "dateFormat",
                        "attributes": {
                            "input": {
                                "type": "dateMath",
                                "attributes": {
                                    "expression": "now"
                                }
                            },
                            "inputFormat": "yyyy-MM-dd'T'HH:mm",
                            "outputFormat": "ISO8601"
                        }
                    }
                ]
            }
        },
        "secondDate": {
            "type": "dateFormat",
            "attributes": {
                "input": {
                    "type": "dateMath",
                    "attributes": {
                        "expression": "now-44d"
                    }
                },
                "inputFormat": "yyyy-MM-dd'T'HH:mm",
                "outputFormat": "ISO8601"
            }
        },
        "operator": "lte",
        "positiveCondition": "true",
        "negativeCondition": "false"
    }
}

```

With the variables **d45** , **d44** , **d42** , and **d38** now defined, the return value of the transform will be:

```json
"value": "#if($d45 == 'true')EXPIRED#{elseif}($d44 == 'true')NOTIFY_1#{elseif}($d42 == 'true')NOTIFY_3#{elseif}($d38 == 'true')NOTIFY_7#{else}ACTIVE#end"

```

Putting it all together, the complete transform looks like this:

```json
{
    "name": "GetLastLoginStatus",
    "type": "static",
    "attributes": {
        "d45": {
            "type": "dateCompare",
            "attributes": {
                "firstDate": {
                    "type": "firstValid",
                    "attributes": {
                        "values": [
                            {
                                "attributes": {
                                    "sourceName": "Entra ID",
                                    "attributeName": "lastSignInDateTime"
                                },
                                "type": "accountAttribute"
                            },
                            {
                                "type": "dateFormat",
                                "attributes": {
                                    "input": {
                                        "type": "dateMath",
                                        "attributes": {
                                            "expression": "now"
                                        }
                                    },
                                    "inputFormat": "yyyy-MM-dd'T'HH:mm",
                                    "outputFormat": "ISO8601"
                                }
                            }
                        ]
                    }
                },
                "secondDate": {
                    "type": "dateFormat",
                    "attributes": {
                        "input": {
                            "type": "dateMath",
                            "attributes": {
                                "expression": "now-45d"
                            }
                        },
                        "inputFormat": "yyyy-MM-dd'T'HH:mm",
                        "outputFormat": "ISO8601"
                    }
                },
                "operator": "lte",
                "positiveCondition": "true",
                "negativeCondition": "false"
            }
        },
        "d44": {
            "type": "dateCompare",
            "attributes": {
                "firstDate": {
                    "type": "firstValid",
                    "attributes": {
                        "values": [
                            {
                                "attributes": {
                                    "sourceName": "Entra ID",
                                    "attributeName": "lastSignInDateTime"
                                },
                                "type": "accountAttribute"
                            },
                            {
                                "type": "dateFormat",
                                "attributes": {
                                    "input": {
                                        "type": "dateMath",
                                        "attributes": {
                                            "expression": "now"
                                        }
                                    },
                                    "inputFormat": "yyyy-MM-dd'T'HH:mm",
                                    "outputFormat": "ISO8601"
                                }
                            }
                        ]
                    }
                },
                "secondDate": {
                    "type": "dateFormat",
                    "attributes": {
                        "input": {
                            "type": "dateMath",
                            "attributes": {
                                "expression": "now-44d"
                            }
                        },
                        "inputFormat": "yyyy-MM-dd'T'HH:mm",
                        "outputFormat": "ISO8601"
                    }
                },
                "operator": "lte",
                "positiveCondition": "true",
                "negativeCondition": "false"
            }
        },
        "d42": {
            "type": "dateCompare",
            "attributes": {
                "firstDate": {
                    "type": "firstValid",
                    "attributes": {
                        "values": [
                            {
                                "attributes": {
                                    "sourceName": "Entra ID",
                                    "attributeName": "lastSignInDateTime"
                                },
                                "type": "accountAttribute"
                            },
                            {
                                "type": "dateFormat",
                                "attributes": {
                                    "input": {
                                        "type": "dateMath",
                                        "attributes": {
                                            "expression": "now"
                                        }
                                    },
                                    "inputFormat": "yyyy-MM-dd'T'HH:mm",
                                    "outputFormat": "ISO8601"
                                }
                            }
                        ]
                    }
                },
                "secondDate": {
                    "type": "dateFormat",
                    "attributes": {
                        "input": {
                            "type": "dateMath",
                            "attributes": {
                                "expression": "now-42d"
                            }
                        },
                        "inputFormat": "yyyy-MM-dd'T'HH:mm",
                        "outputFormat": "ISO8601"
                    }
                },
                "operator": "lte",
                "positiveCondition": "true",
                "negativeCondition": "false"
            }
        },
        "d38": {
            "type": "dateCompare",
            "attributes": {
                "firstDate": {
                    "type": "firstValid",
                    "attributes": {
                        "values": [
                            {
                                "attributes": {
                                    "sourceName": "Entra ID",
                                    "attributeName": "lastSignInDateTime"
                                },
                                "type": "accountAttribute"
                            },
                            {
                                "type": "dateFormat",
                                "attributes": {
                                    "input": {
                                        "type": "dateMath",
                                        "attributes": {
                                            "expression": "now"
                                        }
                                    },
                                    "inputFormat": "yyyy-MM-dd'T'HH:mm",
                                    "outputFormat": "ISO8601"
                                }
                            }
                        ]
                    }
                },
                "secondDate": {
                    "type": "dateFormat",
                    "attributes": {
                        "input": {
                            "type": "dateMath",
                            "attributes": {
                                "expression": "now-38d"
                            }
                        },
                        "inputFormat": "yyyy-MM-dd'T'HH:mm",
                        "outputFormat": "ISO8601"
                    }
                },
                "operator": "lte",
                "positiveCondition": "true",
                "negativeCondition": "false"
            }
        },
        "value": "#if($d45 == 'true')EXPIRED#{elseif}($d44 == 'true')NOTIFY_1#{elseif}($d42 == 'true')NOTIFY_3#{elseif}($d38 == 'true')NOTIFY_7#{else}ACTIVE#end"
    },
    "internal": false
}

```

To put it to use, we map the transform to a custom identity attribute. In this example we’ll call that attribute **lastLoginAdviceStatus** ; the value it holds ( **ACTIVE** , **NOTIFY\_7** , **NOTIFY\_3** , **NOTIFY\_1** or **EXPIRED** ) is what the workflow will react to.

 ![image](https://global.discourse-cdn.com/sailpoint/original/3X/1/6/16fadbbd171492723dd81b2abb8e272ad2d8761c.png)

A quick note on timing: because this is a static transform mapped to an identity attribute, its value is recalculated whenever the identity is refreshed, not continuously. The **now** reference inside each **dateMath** expression is resolved at refresh time, so the comparison window slides forward by one day on each daily refresh. In practice the attribute holds a stable value between refreshes and only changes when the identity actually crosses one of the 38/42/44/45-day boundaries. For the 7-, 3-, and 1-day notifications to land on the correct day, make sure that the Entra ID source is aggregated often enough that **lastSignInDateTime** stays current — a stale input date delays every status transition by however long the source goes without aggregating.

**Validating the transform**

Before wiring up the workflow, it helps to confirm the transform returns what we expect. Assuming it is evaluated on **June 8, 2026** , the four thresholds resolve to: **now-45d** → April 24, **now-44d** → April 25, **now-42d** → April 27, and **now-38d** → May 1.

| Last sign-in | Days inactive | d45 | d44 | d42 | d38 | Result |
| --- | --- | --- | --- | --- | --- | --- |
| Jun 5, 2026 | 3 | false | false | false | false | ACTIVE |
| May 1, 2026 | 38 | false | false | false | true | NOTIFY\_7 |
| Apr 27, 2026 | 42 | false | false | true | true | NOTIFY\_3 |
| Apr 25, 2026 | 44 | false | true | true | true | NOTIFY\_1 |
| Apr 24, 2026 | 45 | true | true | true | true | EXPIRED |
| _no Entra ID account_ | — | false | false | false | false | ACTIVE |

The last row shows the First Valid fallback in action: with no **lastSignInDateTime** to read, the transform substitutes today’s date, so the identity resolves to ACTIVE instead of being wrongly expired.

**Acting on each status**

Finally, a workflow takes action depending on the value of this attribute. It is triggered by the Identity Attributes Changed event, and filtered so that it only fires when **lastLoginAdviceStatus** changes value. Because the trigger reacts to the _transition_ rather than to the current value, each notification is sent exactly once — the moment the identity moves into a new status. The first step retrieves the identity’s attributes (email, display name, manager, and so on); a set of comparators then branches on the new value:

- **NOTIFY\_7** → send the “7 days remaining” reminder
- **NOTIFY\_3** → send the “3 days remaining” reminder
- **NOTIFY\_1** → send the “1 day remaining” reminder
- **EXPIRED** → disable the account (optionally sending a final notice)
- **ACTIVE** → no action; this transition simply means the user signed in again and the counter reset

Splitting the responsibility this way keeps the logic clean: the transform decides _what_ status an identity is in, and the workflow decides _what to do_ about it.

 ![image](https://global.discourse-cdn.com/sailpoint/original/3X/b/d/bd629bd03fbd3a01a7ba97a66efda822cadb3ebe.png)

 ![image](https://global.discourse-cdn.com/sailpoint/original/3X/e/3/e38356f8962087bc2d4ad7a02e2e4b1abcf90d83.png)

 ![image](https://global.discourse-cdn.com/sailpoint/original/3X/d/0/d0bfaa823a26e41c3c0ace824903de735d4dbf54.png)

**Safety note:** Disabling an account is a destructive action. Before enabling the **EXPIRED** branch in production, test the disable step against a small, controlled population first — a handful of test identities or a limited pilot group whose scope you can predict — and confirm that only the intended accounts are disabled. Widen the scope to your full population only once you’ve validated the full flow end to end.

# **Considerations**

The goal of this blog post is to present a transform that efficiently handles last-login processing. The ready-to-use transform is attached. The workflow can be built as shown above; since the possibilities are virtually endless, it is not included here.

# **Useful links**

> **[Date Compare | SailPoint Developer Community](https://developer.sailpoint.com/docs/extensibility/transforms/operations/date-compare/)**
>
> Compare two dates and return a calculated value.

> **[Date Math | SailPoint Developer Community](https://developer.sailpoint.com/docs/extensibility/transforms/operations/date-math/)**
>
> Add, subtract, and round components of a timestamp's incoming value.

> **[Static | SailPoint Developer Community](https://developer.sailpoint.com/docs/extensibility/transforms/operations/static/)**
>
> Return a fixed string value.

[last-login.json](https://developer.sailpoint.com/discuss/uploads/short-url/pnMKfbK2m4uyq7eqYO5j1uOM9co.json) (8.0 KB)
