Inactive Account Day Threshold Handling Using Transforms

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 (#):

"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:

"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:

"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:

"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:

{
    "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.

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.

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

last-login.json (8.0 KB)

1 Like