Introduction
Managing user lifecycle events, such as onboarding and role changes, is a critical aspect of identity governance. Microsoft Entra ID Governance provides Lifecycle Workflows, enabling administrators to automate user-related tasks. However, Entra ID Governance workflows do not natively support assigning on-premises Active Directory (AD) groups to users. To bridge this gap, Lifecycle Workflow Custom Task Extension allow you to integrate Automation PowerShell Runbooks and APIs to automate such processes. Lifecycle Workflow Custom Task Extension are a good way to integrate more then just native building blocks.
Table of Contents
This blog will guide you through the process of creating a Custom Task Extension to assign on-premise AD groups to users automatically.

Prerequisites
Before implementing the custom task extension, ensure you have:
- Microsoft Entra ID Governance with Lifecycle Workflows enabled.
- Hybrid Identity setup using Azure AD Connect.
- Microsoft Graph API permissions.
- A service account with required permissions to modify on-premise AD groups.
- Logic App to create an Automation Job
- Azure Automation to execute the script.
Define the Use Case
We aim to automate group assignments in an on-premise Active Directory environment when a new user joins an organization.
Example Scenario
- A new employee joins the company and must join specific security groups in the on-premise AD.
- The Lifecycle Workflow triggers an action based on an event (e.g., new hire, promotion, or department change).
Create the Custom Task Extension
Lifecycle Workflows support Custom Task Extensions that execute external scripts via APIs or automation tools. Create a custom extension via Lifecycle Workflow Blade with the following settings. This requires sufficient rights to allow the system to create a Logic App in the background.

Create an Automation Account
To run a PowerShell script on an on-premise system, you need an Automation account. Complete the following steps in preparation.
- Hybrid Worker Installation on an On-Premise System that can Access the AD DS
- Runbook Preparation for the executing Powershell on the Hybrid Worker
- Shared Credentials Setup for access the AD DS with the right Privileges
Create and publish the following runbook. This requires the DomainAdminCreds (Security Risk, working on a updated Version with gMSA) and the following file structure on the Hybrid Worker:
param (
[string]$UserPrincipalName # UPN from Entra ID (e.g., user@domain.com)
)
# Import Active Directory module f
Import-Module ActiveDirectory
# Retrieve stored credentials from Azure Automation
$Cred = Get-AutomationPSCredential -Name "DomainAdminCreds"
if (-not $Cred) {
Write-Output "ā ERROR: Failed to retrieve domain admin credentials."
exit 2
}
# Define log file path (based on user and timestamp)
$LogFolder = "C:\LifecycleWorkflows\Logs"
if (!(Test-Path $LogFolder)) {
New-Item -ItemType Directory -Path $LogFolder | Out-Null
}
$Timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
$LogFile = "$LogFolder\$UserPrincipalName-$Timestamp.log"
# Function to write log output to file
function Write-Log {
param ([string]$Message)
$LogEntry = "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - $Message"
Write-Output $LogEntry
Add-Content -Path $LogFile -Value $LogEntry
}
Write-Log "š Starting script for user: $UserPrincipalName"
# Load JSON file
$JsonFilePath = "C:\LifecycleWorkflows\access-packages.json"
$JsonData = Get-Content -Path $JsonFilePath | ConvertFrom-Json
# Fetch AD User using domain admin credentials
$ADUser = Get-ADUser -Filter {UserPrincipalName -eq $UserPrincipalName} -Credential $Cred -Properties SamAccountName, DisplayName, Company, Department, MemberOf
if (-not $ADUser) {
Write-Log "ā ERROR: No matching user found in AD for UPN $UserPrincipalName"
exit 2
}
# Extract User Details
$UserCompany = $ADUser.Company
$UserDepartment = $ADUser.Department
$UserGroups = $ADUser.MemberOf
Write-Log "ā
User Found: $($ADUser.DisplayName)"
Write-Log " š¢ Company: $UserCompany"
Write-Log " šļø Department: $UserDepartment"
Write-Log "-------------------------------------"
# Function to Add User to AD Group
function Add-UserToGroup {
param ($GroupName)
# Check if group exists in AD
$ADGroup = Get-ADGroup -Filter { Name -eq $GroupName } -ErrorAction SilentlyContinue
if ($ADGroup) {
# Check if user is already a member
if ($UserGroups -contains $ADGroup.DistinguishedName) {
Write-Log " ā¹ļø User was already in group: $GroupName (skipping add)"
return "AlreadyInGroup"
} else {
# Try adding user
try {
Add-ADGroupMember -Identity $GroupName -Members $ADUser.SamAccountName -ErrorAction Stop
Write-Log " ā
Successfully added user to group: $GroupName"
return "Success"
} catch {
Write-Log " ā ERROR adding user to group: $GroupName - $_"
return "Failure"
}
}
} else {
Write-Log " ā ļø WARNING: Group '$GroupName' not found in AD!"
return "Failure"
}
}
# Track success/failure
$SuccessCount = 0
$FailureCount = 0
$AlreadyInGroupCount = 0
# Assign Default Access Packages
Write-Log "`nš¹ Assigning Default Access Packages..."
foreach ($group in $JsonData.defaultAccessPackage.Default) {
$result = Add-UserToGroup -GroupName $group
if ($result -eq "Success") {
$SuccessCount++
} elseif ($result -eq "Failure") {
$FailureCount++
} elseif ($result -eq "AlreadyInGroup") {
$AlreadyInGroupCount++
}
}
# Assign Company Access Packages
Write-Log "`nš¹ Assigning Company Access Packages..."
if ($UserCompany -and $JsonData.companyAccessPackages.PSObject.Properties.Name -contains $UserCompany) {
foreach ($group in $JsonData.companyAccessPackages.$UserCompany) {
$result = Add-UserToGroup -GroupName $group
if ($result -eq "Success") {
$SuccessCount++
} elseif ($result -eq "Failure") {
$FailureCount++
} elseif ($result -eq "AlreadyInGroup") {
$AlreadyInGroupCount++
}
}
} else {
Write-Log " ā No company-specific access packages found for: $UserCompany"
}
# Assign Department Access Packages
Write-Log "`nš¹ Assigning Department Access Packages..."
if ($UserDepartment -and $JsonData.departmentAccessPackages.PSObject.Properties.Name -contains $UserDepartment) {
foreach ($group in $JsonData.departmentAccessPackages.$UserDepartment) {
$result = Add-UserToGroup -GroupName $group
if ($result -eq "Success") {
$SuccessCount++
} elseif ($result -eq "Failure") {
$FailureCount++
} elseif ($result -eq "AlreadyInGroup") {
$AlreadyInGroupCount++
}
}
} else {
Write-Log " ā No department-specific access packages found for: $UserDepartment"
}
# Final Report
Write-Log "`nšÆ Processing complete!"
Write-Log "ā
Successfully added to $SuccessCount groups."
Write-Log "ā¹ļø Already in $AlreadyInGroupCount groups."
Write-Log "ā Failed to add to $FailureCount groups."
# Exit codes for Lifecycle Workflows:
# 0 = Success (All groups assigned)
# 1 = Partial Success (Some groups failed)
# 2 = Failure (No groups assigned)
if ($FailureCount -eq 0 -and ($SuccessCount -gt 0 -or $AlreadyInGroupCount -gt 0)) {
# SUCCESS: All groups were either added or the user was already in them
Write-Log "š SUCCESS: User assigned to all required groups!"
exit 0
} elseif ($SuccessCount -gt 0 -or $AlreadyInGroupCount -gt 0) {
# PARTIAL SUCCESS: Some groups were added or already existed, but others failed
Write-Log "ā ļø PARTIAL SUCCESS: Some groups failed!"
exit 1
} else {
# FAILURE: No groups could be assigned
Write-Log "ā FAILURE: No groups could be assigned!"
exit 2
}
Start-ADSyncSyncCycle -PolicyType Delta
Prepare you Environment for the Script
Here is a small overview of how the file structure should look on the worker, logs are written and the standard groups per company and department are listed in the .json file.
- C:\LifecycleWorkflows\access-packages.json
- C:\LifecycleWorkflows\Logs
This solution only assigns the user the groups that he actually needs, i.e. it checks which user attributes have been set and which groups have been released for this e.g. department.
This is handled with the following .json file.
{
"defaultAccessPackage": {
"Default": [
"cloudfil",
"All-Users-All-Companies",
"All-Users-cloudfil-Company-SharePointSite"
]
},
"companyAccessPackages": {
"cloudfil.ch": [
"GSG_IT-Department",
"GSG_CAN_365"
],
"contoso.com": [
"contoso-MarketingDokumentationen",
"EID-Contoso-Integration-2026"
]
},
"departmentAccessPackages": {
"Digital Transformation": [
"EID-MarketingMaterials"
],
"HR": [
"EID-ITProjects",
"EID-MarketingRecuritmentStuff"
],
"IT": [
"EID-HR-Tool-Project",
"GSG_POL_365"
]
}
}
How do I create such a .json file, this is quite simple here I have built a solution called Access Package Builder. Where you can easily export such a .json file, have a look:
https://accesspackagebuilder.azurewebsites.net
Modify and Prepare the Logic App
Now everything is prepared and can be linked to the Logic app. The only parameter that is passed to the runbook is the UserPrincipalName – see below.

Save and test!
Test the Workflow (On-Demand)
The workflow can be easily tested (on-demand). If you want to equip it later with other triggers / automatisms, this is very possible. See the first image in the blog post.
- Open Workflow
- Navigate to Execution conditions
- Select your Test User which is a AD DS User
- Run workflow

To check that everything has worked properly, there are logs in several places. From the lifecycle workflow status to the automation accounts runbook outputs to the end on the hybrid worker where a log file is written for each user.
Conclusion
Unfortunately, Microsoft’s Identity Governance Solution in Entra ID does not provide the desired solutions when it comes to hybrid infrastructures. Access packages can only be structured with cloud groups. However, if you think the process through to the end, you often also need to assign on-premise groups in the JML processes.
Here I find such a solution very helpful and useful, and it also runs very reliably. If you are interested, take a look at more blog posts here: