Expired Guest Access Cleanup
Scheduled flow checks for Azure AD guest users whose access has expired and removes them, logging actions to SharePoint.
Provided as-is, without warranty of any kind. Review and test each pattern in a non-production environment before deploying it to live automations. See our Terms.
Overview
A scheduled (weekly) Power Automate cloud flow that audits the tenant for guest user accounts whose createdDateTime is older than a configurable threshold and removes them via Microsoft Graph. Each removal is logged to a SharePoint audit list, and a summary email is sent to IT after every run with activity. The flow ships Off — turning it on requires only configuring the Graph app registration secrets and authorizing the SharePoint + Outlook connection references.
Use Case
Microsoft Entra ID tenants that allow B2B collaboration accumulate guest user accounts indefinitely unless governance is enforced. Stale guests inflate the user directory, complicate license reporting, and represent an avoidable security surface (compromised partner accounts, departed contractors, abandoned vendor invitations). This flow gives IT Admins a low-touch, auditable cleanup loop.
The flow is ideal for teams that:
- Define an expiration window (default 365 days from createdDateTime).
- Run weekly on Monday at 8:00 AM Eastern.
- Delete via Graph and log every action to SharePoint with the run ID, age in days, and Graph response code for traceability.
- Notify IT only when there is activity (removals or failures).
Flow Architecture
Recurrence_Weekly_Cleanup_Schedule
RecurrenceFires every Monday at 8:00 AM Eastern.
Initialize_varCutoffDate
Initialize variable (string)Computes ISO 8601 cutoff using getPastTime(GuestExpirationDays, 'Day', ...).
Initialize_varRemovedCount
Initialize variable (integer)Counter for successful removals.
Initialize_varFailedCount
Initialize variable (integer)Counter for delete-or-log failures.
Initialize_varRemovedGuests
Initialize variable (array)Captures the audit details of every removed guest.
Initialize_varRunId
Initialize variable (string)Stores `workflow().run.name` for log correlation.
HTTP_Get_Expired_Guest_Users
HTTP (Active Directory OAuth)GET https://graph.microsoft.com/v1.0/users with $filter=userType eq 'Guest' and createdDateTime lt {cutoff} and $select=id,displayName,mail,userPrincipalName,createdDateTime,userType.
Parse_JSON_Guest_Users_Response
Parse JSONSchema-binds the Graph response so each guest object is type-safe in downstream actions.
Foreach_Expired_Guest_User
Apply to each (concurrency 1)For each expired guest in body('Parse_JSON_Guest_Users_Response')?['value']: builds an audit object via Compose_Guest_Details, then runs Scope_Delete_Guest which wraps HTTP_Delete_Guest_User (DELETE https://graph.microsoft.com/v1.0/users/{id}), Append_To_Removed_Guests_Array, Increment_Removed_Count, and Log_Removal_To_SharePoint (PostItem to the audit list). A runAfter Failed/TimedOut/Skipped increments varFailedCount when the scope does not succeed. Sequential execution keeps the audit list ordered and avoids Graph rate-limit hot-spots.
Environment Variables
| Schema name | Type | Default | Description |
|---|---|---|---|
| flowlibs_GuestExpirationDays | String | 365 | Days since `createdDateTime` to consider a guest expired. |
| flowlibs_GuestCleanupListName | String | FlowLibs - Expired Guest Cleanup Log | SharePoint list title used as `table` in PostItem. |
| flowlibs_SharePointSiteURL | String | https://your-tenant.sharepoint.com | Tenant-wide SharePoint site URL bound to PostItem `dataset`. |
| flowlibs_NotificationEmail | String | admin@your-tenant.onmicrosoft.com | Recipient for summary email. |
| flowlibs_GraphTenantId | String | <your-tenant-id> | OAuth `tenant` for both Graph HTTP calls. |
| flowlibs_GraphClientId | String | 00000000-0000-0000-0000-000000000000 | App registration client ID. |
| flowlibs_GraphClientSecret | String | <configure> | App registration secret. Replace with your client secret before turning the flow On. |
Connectors & Connections
| Connector | API name | Actions used |
|---|---|---|
| SharePoint Online | shared_sharepointonline | PostItem (Logs every removal to the audit list) |
| Office 365 Outlook | shared_office365 | SendEmailV2 (Sends summary email to IT) |
| HTTP (built-in) | http | GET /v1.0/users (Lists expired guests via Microsoft Graph (Active Directory OAuth)) DELETE /v1.0/users/{id} (Deletes a guest user via Microsoft Graph) |
Note — All connections are referenced as solution connection references; the flow is portable between environments as long as a connection is mapped at import time.
Customization Guide
Almost every realistic variant of this flow can be implemented by changing environment variable values. A few cases require small edits inside the flow definition — those are called out explicitly below.
- Register an Entra ID app
- Register an Entra ID app with Microsoft Graph application permissions User.ReadWrite.All and grant admin consent. Copy the tenant ID, app (client) ID, and a fresh client secret.
- Update the three Graph env vars
- In the solution, set flowlibs_GraphTenantId, flowlibs_GraphClientId, and flowlibs_GraphClientSecret using Current Value (leave Default Value alone so other flows keep working).
- Tune the expiration policy
- Edit flowlibs_GuestExpirationDays (raise to 730 for a softer policy, lower to 90 for hot tenants).
- Authorize the connection references
- Open the solution and set SharePoint and Office 365 Outlook to active connections owned by the user or service account that runs the flow.
- Confirm the SharePoint audit list exists
- Verify the tenant root has a list named FlowLibs - Expired Guest Cleanup Log with columns: GuestObjectId, GuestDisplayName, GuestEmail, GuestUserPrincipalName, GuestCreatedDateTime, AgeInDays, DeletionStatus, GraphResponseCode, FlowRunId. Provisioned by this build.
- Open the flow in the designer once
- Open the flow once so the SharePoint PostItem dynamic-schema warning resolves (Power Automate caches the column list on first save), then re-Save.
- Turn the flow On
- First scheduled run is the next Monday at 08:00 Eastern; trigger a manual test run after-hours first.
- Optional: switch from full delete to soft block
- Replace the HTTP_Delete_Guest_User body with a PATCH to /users/{id}
Key Expressions
The flow is intentionally light on Power Fx / WDL gymnastics — the heaviest expressions are the branch-name concatenation and the approval outcome check. They are listed below in the order they appear in the flow.
EXPR.01varCutoffDate
ISO 8601 timestamp GuestExpirationDays ago.
EXPR.02Graph $filter
OData filter passed to Graph (`''` escapes single-quote in Power Automate).
EXPR.03Delete URI
Per-iteration DELETE target.
EXPR.04AgeInDays (logged)
Integer days between createdDateTime and now (864e9 = 100ns ticks per day).
EXPR.05Email Title fallback
Always populates the SharePoint Title column even if Graph returns a null displayName.
EXPR.06Run ID capture
Per-run GUID written to FlowRunId for cross-system correlation.
EXPR.07Failure path runAfter
Increments varFailedCount without aborting the Foreach.
Comments
Sign in to join the conversation.
Sign inNo comments yet. Be the first to share your experience with this flow.