Not as a single feature, but doable with some glue code. FusionAuth has no built-in “inactivity warning” workflow or scheduler for this.

It does give you the pieces: a lastLoginInstant on every user, a user search API, an email send API, and user deactivation. You wire them together with a small scheduled job (cron, Lambda, GitHub Action, etc.) outside FusionAuth.

Set up a daily job that does three things:

Finds users whose last activity was about 55 days ago and who haven’t been warned yet. Sends them the warning email and records that you did. Finds users who are 60+ days inactive and marks them dormant.

If a user logs in during the 5-day window, their lastLoginInstant updates and they drop out of the step 3 query automatically.

Use the User Search API with an Elasticsearch/OpenSearch query on lastLoginInstant (milliseconds since epoch). This needs the Elasticsearch/OpenSearch search engine. The database search engine doesn’t support range queries on this field.

POST /api/user/search { "search": { "numberOfResults": 500, "query": "{\"bool\":{\"must\":[{\"range\":{\"lastLoginInstant\":{\"lt\":1760000000000}}}],\"must_not\":[{\"term\":{\"data.dormantWarningSent\":true}}]}}" } }

Compute the lt value in your job as now - 55 days (so replace 1760000000000). Users who have never logged in won’t have a lastLoginInstant, so decide whether to fall back to insertInstant for them.

You can use FusionAuth to send the email or your own system. If you choose the former, create an email template in Customizations → Email Templates, then call the Send Email API with the matching user IDs:

POST /api/email/send/{emailTemplateId} { "userIds": ["<id1>", "<id2>"], "requestData": { "daysInactive": 55, "daysUntilDormant": 5 } }

Your template body can then use FreeMarker:

Hi ${user.firstName!"there"}, As part of our routine account security measures, we monitor accounts for inactivity. We haven't detected activity on your account in the last ${daysInactive} days, and it is scheduled to be marked dormant in ${daysUntilDormant} days unless activity is detected. Log in to keep your account active: https://login.example.com

requestData values are available at the top level of the template, so you can reuse the template if you change the thresholds later.

After a successful send, patch the user to record that they've been notified so you don't send them multiple emails:

PATCH /api/user/{userId} { "user": { "data": { "dormantWarningSent": true, "dormantWarningSentAt": 1760000000000 } } }

The must_not clause in the search query above then skips them. If the user logs in, clear the flag so they can be warned again in the future. A user.login.success webhook receiver that uses the API is a clean place to do that.

As far as what you actually do when the user is marked dormant, you have two options:

Deactivate the user. DELETE /api/user/{id} (soft delete) sets active=false, which blocks login. To reactivate, call PUT /api/user/{id}?reactivate=true. Deactivation also fires a user.deactivate event that you can consume via webhook to notify downstream systems. Flag only. Set user.data.dormant = true and handle it in your app where you might do a step-up or force an account recovery. This is gentler and keeps the account usable after a step-up.

For a true “close the account” outcome, deactivate first. Hard-delete (?hardDelete=true) only after a retention period that satisfies your compliance requirements.

Caveats:

lastLoginInstant updates when a user authenticates. Silent token refreshes using an existing refresh token don’t necessarily count as logins, so a user with a long-lived session in your app could look inactive. If that matters, track your own activity timestamp in user.data (updated by your app or a webhook) and query that instead. Test this against your own setup before relying on it.

user.lastLoginInstant is global to the user within a tenant. Each registration also has its own lastLoginInstant (user.registrations[].lastLoginInstant). If “inactive” should mean inactive in a specific application, query on that field and filter by applicationId.

Run the job daily and use a window, not an exact day match, so a missed run doesn’t skip anyone.

Page through search results (startRow) rather than assuming one page covers everyone.

Test with a user whose lastLoginInstant you’ve backdated in a non-production tenant.

Make sure your Send Email API key has only the endpoints it needs (search, send email, patch user, deactivate).