EASYENTRA Blog

News & Updates

User Onboarding with EasyEntra Templates and Beyond 

EasyEntra user onboarding with templates, powershell and forms

Ask any IT admin what eats up their time, and user onboarding will be somewhere near the top of the list. It’s not complicated work, but it’s fiddly and repetitive. A single new hire can mean jumping across systems, Active Directory, Microsoft Entra ID, Exchange Online, Microsoft 365 licensing, and more. When handled manually, it often turns into hours of clicking, checking, and second-guessing.

But it doesn’t have to stay that way! With the right setup in place, onboarding tends to become a quick, repeatable process, often taking just minutes instead of hours. This post walks through how to get there using EasyEntra’s template-driven onboarding, with PowerShell providing the final layer of control and customization.

The Foundation: EasyEntra PowerShell + Virtual User Templates

Before getting into how the script works, it’s worth understanding what makes EasyEntra’s approach so effective, because the PowerShell and the virtual user template system are doing a lot of work together.

What EasyEntra User Templates Handle

EasyEntra’s virtual user templates are the centerpiece of this approach. A template is essentially a “blueprint user” in AD or Entra ID, pre-configured with everything a particular role or department needs. Here’s what gets handled automatically when you provision a user from a template:

AD and Entra ID Group Memberships – The new user is added to every security group and Microsoft 365 group that the template user belongs to. SharePoint access, Teams channels, distribution lists, all of it lands correctly on day one.

License Assignment – Licenses are applied based on the template, including any specific service plan inclusions or exclusions you’ve configured.

Mailbox Provisioning and Configuration – Exchange Online mailbox creation is triggered automatically, and mailbox settings, like delegation or archiving are carried over from the template.

Conditional Access and Security Policies – Because group memberships are replicated, the new user immediately inherits the same Conditional Access policy scope as the template user.

Profile Attributes – Department, job title, usage location, and other standard AD or Entra ID attributes are populated from the template, keeping your directory clean and consistent.

Where EasyEntra PowerShell Takes Over

Once EasyEntra has done the heavy lifting, there are always a handful of organization-specific attributes and integrations to handle. This is where a small custom PowerShell layer adds enormous value.

When you create a new user, EasyEntra copies everything from the selected template to the new account. Instead of setting up each part manually, you just select a template and let the system do the rest. Here’s how simple it is:

#Cloud user setup 
Invoke-EECreateEntraUserFromTemplate -DisplayName "John Lennon" -TemplateName "Liverpool HR" 
#Hybrid user setup 
Invoke-EECreateHybridUserFromTemplate -DisplayName "John Lennon" -TemplateName "Liverpool HR" 

With just one command, a fully configured user is created. 

Two Ways to Use It

Choose what fits your workflow. There are two common ways teams use EasyEntra. Both are simple and use the same core idea.

1. A PowerShell Form for Ad-Hoc Onboarding

This approach uses a small form where someone from the support team can enter basic unique details such as the user’s display name, employee ID, or mobile phone. They don’t need to know PowerShell or technical steps. They just fill in the form and click the button. Behind the scenes, EasyEntra creates the user and automatically runs any additional steps. This works well when onboarding requests come in occasionally and need a human to trigger them.

2. Fully Automated Scripting: Triggered From External Systems

In this approach, no one needs to manually create users. The process is triggered automatically. For example, when a new employee is added to an HR system, the script runs and instantly creates the user. The script handles everything from start to finish, including any additional setup steps. This is ideal for organizations that want a completely hands-free process.

The 95 and 5 Rule

A helpful way to think about this setup is the 95 and 5 rule. EasyEntra handles about 95% of the work from a single CmdLet with two parameters. This includes things like groups, licenses, email setup, and standard configurations that are the same for every user in a role.

The remaining 5% is custom work. This could be things like adding an employee ID, sending a welcome email, or connecting to another system your company uses. EasyEntra takes care of the heavy lifting, while your custom logic handles the unique parts.

Here’s a complete script that brings this onboarding flow together:

Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing

# ── Infrastructure ────────────────────────────────────────────────────────────
# Runs a PowerShell cmdlet in the background without freezing the form.
# Calls $OnComplete on the UI thread when done, passing the result and $Context.
# Controls listed in $ReEnableOnDone are re-enabled automatically.
# No need to modify this function.
function Invoke-Async {
    param(
        [string]                           $Module,
        [scriptblock]                      $Work,
        [object[]]                         $Arguments      = @(),
        [scriptblock]                      $OnComplete,
        [hashtable]                        $Context        = @{},
        [System.Windows.Forms.Control[]]   $ReEnableOnDone = @()
    )
    $global:_async_onComplete     = $OnComplete
    $global:_async_context        = $Context
    $global:_async_reEnable       = $ReEnableOnDone
    $global:_async_fired          = $false
    $iss = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault()
    $iss.ImportPSModule($Module)
    $global:_async_rs = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace($iss)
    $global:_async_rs.Open()
    $global:_async_ps = [powershell]::Create()
    $global:_async_ps.Runspace = $global:_async_rs
    [void] $global:_async_ps.AddScript($Work)
    $Arguments | ForEach-Object { [void] $global:_async_ps.AddArgument($_) }
    $global:_async_result = $global:_async_ps.BeginInvoke()
    $global:_async_timer          = New-Object System.Windows.Forms.Timer
    $global:_async_timer.Interval = 250
    $global:_async_timer.Add_Tick({
        if (-not $global:_async_result.IsCompleted) { return }
        if ($global:_async_fired) { return }
        $global:_async_fired = $true
        $global:_async_timer.Stop()
        $global:_async_timer.Dispose()
        try     { $output = $global:_async_ps.EndInvoke($global:_async_result) }
        catch   { $output = $null }
        finally { $global:_async_ps.Dispose(); $global:_async_rs.Dispose() }
        & $global:_async_onComplete ($output | Select-Object -Last 1) $global:_async_context
        $global:_async_reEnable | ForEach-Object { $_.Enabled = $true }
    })
    $global:_async_timer.Start()
}
function Add-LabeledControl {
    param($Form, $LabelText, $Control, $Y)
    $lbl          = New-Object System.Windows.Forms.Label
    $lbl.Text     = $LabelText
    $lbl.Location = New-Object System.Drawing.Point(20, ($Y + 4))
    $lbl.AutoSize = $true
    $lbl.Font     = New-Object System.Drawing.Font("Segoe UI", 9)
    $Control.Location = New-Object System.Drawing.Point(180, $Y)
    $Control.Width    = 320
    $Form.Controls.AddRange(@($lbl, $Control))
}
function Write-Log {
    param($TextBox, $Message)
    $TextBox.AppendText("$(Get-Date -Format 'HH:mm:ss')  $Message`n")
    $TextBox.ScrollToCaret()
}
# ── Connect to Microsoft Graph ────────────────────────────────────────────────
try {
    Write-Host "Connecting to Microsoft Graph..."
    Connect-MgGraph -Scopes "User.Read.All", "User.ReadWrite.All" -NoWelcome
    Write-Host "Connected successfully."
} catch {
    [System.Windows.Forms.MessageBox]::Show(
        "Failed to connect to Microsoft Graph.`n`n$_",
        "Connection Error", "OK", "Error")
    exit
}
# ── Form chrome ───────────────────────────────────────────────────────────────
$form               = New-Object System.Windows.Forms.Form
$form.Text          = "EasyEntra — New User Onboarding"
$form.Size          = New-Object System.Drawing.Size(540, 540)
$form.StartPosition = "CenterScreen"
$form.FormBorderStyle = "FixedDialog"
$form.MaximizeBox   = $false
$form.Font          = New-Object System.Drawing.Font("Segoe UI", 9)
$rtbOutput            = New-Object System.Windows.Forms.RichTextBox
$rtbOutput.Location   = New-Object System.Drawing.Point(20, 248)
$rtbOutput.Size       = New-Object System.Drawing.Size(490, 245)
$rtbOutput.ReadOnly   = $true
$rtbOutput.BackColor  = [System.Drawing.Color]::FromArgb(30, 30, 30)
$rtbOutput.ForeColor  = [System.Drawing.Color]::FromArgb(220, 220, 180)
$rtbOutput.Font       = New-Object System.Drawing.Font("Consolas", 9)
$rtbOutput.ScrollBars = "Vertical"
$lblOutput          = New-Object System.Windows.Forms.Label
$lblOutput.Text     = "Output"
$lblOutput.Location = New-Object System.Drawing.Point(20, 225)
$lblOutput.AutoSize = $true
$btnRun          = New-Object System.Windows.Forms.Button
$btnRun.Text     = "Create User"
$btnRun.Location = New-Object System.Drawing.Point(180, 180)
$btnRun.Width    = 140
$btnRun.Height   = 32
$form.Controls.AddRange(@($rtbOutput, $lblOutput, $btnRun))
# ── CUSTOMIZE: Input fields ───────────────────────────────────────────────────
# Add, remove or rename fields here. Add-LabeledControl places a label + control
# at the given Y position (each row is ~40px).
$txtDisplayName            = New-Object System.Windows.Forms.TextBox
$cboTemplate               = New-Object System.Windows.Forms.ComboBox
$cboTemplate.DropDownStyle = "DropDownList"
$txtEmployeeId             = New-Object System.Windows.Forms.TextBox
$txtMobile                 = New-Object System.Windows.Forms.TextBox
Add-LabeledControl $form "Display name *"  $txtDisplayName  20
Add-LabeledControl $form "Template *"      $cboTemplate     60
Add-LabeledControl $form "Employee ID"     $txtEmployeeId   100
Add-LabeledControl $form "Mobile number"   $txtMobile       140
# ── Template loader ───────────────────────────────────────────────────────────
$form.Add_Load({
    Write-Log $rtbOutput "Loading templates from Entra ID..."
    try {
        $templates = Get-MgUser `
            -Filter "startswith(userPrincipalName,'EasyEntra-Template-')" `
            -Select "DisplayName" -All |
            Select-Object -ExpandProperty DisplayName | Sort-Object

        $cboTemplate.Items.Clear()
        $templates | ForEach-Object { [void] $cboTemplate.Items.Add($_) }
        if ($cboTemplate.Items.Count -gt 0) {
            $cboTemplate.SelectedIndex = 0
            Write-Log $rtbOutput "Found $($templates.Count) template(s). Ready."
        } else {
            Write-Log $rtbOutput "WARNING: No templates found."
        }
    } catch {
        Write-Log $rtbOutput "ERROR loading templates: $_"
    }
})
# ── CUSTOMIZE: Button click ───────────────────────────────────────────────────
# $result   — the object returned by Invoke-EECreateEntraUserFromTemplate
# $ctx      — the hashtable passed as -Context below; add any values you need
#             accessible inside the completion handler
$btnRun.Add_Click({
    $rtbOutput.Clear()
    $displayName  = $txtDisplayName.Text.Trim()
    $templateName = $cboTemplate.SelectedItem
    $employeeId   = $txtEmployeeId.Text.Trim()
    $mobile       = $txtMobile.Text.Trim()
    if ([string]::IsNullOrWhiteSpace($displayName) -or -not $templateName) {
        [System.Windows.Forms.MessageBox]::Show(
            "Display name and Template are required.",
            "Validation", "OK", "Warning")
        return
    }
    $btnRun.Enabled = $false
    Write-Log $rtbOutput "[1/4] Creating user '$displayName' from template '$templateName'..."
    Invoke-Async `
        -Module         "EasyEntraPS" `
        -Work           { param($dn, $tn)
                          Invoke-EECreateEntraUserFromTemplate -DisplayName $dn -TemplateName $tn } `
        -Arguments      @($displayName, $templateName) `
        -Context        @{ log = $rtbOutput; employeeId = $employeeId; mobile = $mobile } `
        -ReEnableOnDone @($btnRun) `
        -OnComplete     {
            param($result, $ctx)
            if ($null -eq $result) {
                Write-Log $ctx.log "FATAL: No result returned."
                return
            }
            Write-Log $ctx.log "[2/4] EasyEntra status: $($result.Status)"
            Write-Log $ctx.log "--- EasyEntra Task Log ---"
            $result.TaskLog | ForEach-Object {
                $line = "[$($_.Time.ToString('HH:mm:ss'))] $($_.Status.ToString().PadRight(9)) $($_.Task)"
                if ($_.Message) { $line += " > $($_.Message)" }
                Write-Log $ctx.log $line
            }
            Write-Log $ctx.log "--- End Task Log ---"
            if ($result.Status -eq "FatalError") {
                Write-Log $ctx.log "Aborting — fatal error returned by EasyEntra."
                return
            }
            # ── CUSTOMIZE: Post-creation attribute patch ──────────────────
            # Add any additional Graph properties here. $result.Identity is
            # the UPN of the newly created user.
            Write-Log $ctx.log "[3/4] Patching Graph attributes..."
            try {
                $patch = @{}
                if ($ctx.employeeId) { $patch['employeeId']  = $ctx.employeeId }
                if ($ctx.mobile)     { $patch['mobilePhone'] = $ctx.mobile     }
                if ($patch.Count -gt 0) {
                    $maxRetries = 5
                    $retryCount = 0
                    $patchSuccess = $false
                    while (-not $patchSuccess -and $retryCount -lt $maxRetries) {
                        try {
                            Update-MgUser -UserId $result.Identity -BodyParameter $patch -ErrorAction Stop
                            $patchSuccess = $true
                            Write-Log $ctx.log "Graph update successful: $($patch.Keys -join ', ')"
                        } catch {
                            $retryCount++
                            Write-Log $ctx.log "User not ready yet, retrying in 10 seconds... ($retryCount/$maxRetries)"
                            Start-Sleep -Seconds 10
                        }
                    }
                    if (-not $patchSuccess) {
                        Write-Log $ctx.log "WARNING: Graph patch failed after $maxRetries retries."
                    }
                } else {
                    Write-Log $ctx.log "No optional attributes to patch."
                }
            } catch {
                Write-Log $ctx.log "WARNING: Graph patch failed: $_"
            }

            Write-Log $ctx.log "[4/4] Done."
        }
})
# ── Launch ────────────────────────────────────────────────────────────────────
[System.Windows.Forms.Application]::EnableVisualStyles()
[void] $

How the Script is Put Together

When you run the script, the first thing it does is connect to Microsoft Graph PowerShell. You will get the standard authentication prompt, and once that’s done, a clean onboarding window opens up.

EasyEntra user onboarding with PowerShell forms.
Example form created via PowerShell to customize user onboarding for the Helpdesk.

The form gives you fields to fill in. The first two fields are all EasyEntra needs to do its job:

  • Display name – the display name of the new user we’re creating. Combined with the template information, this input automatically aligns email address, UPN, sAMAccountName, and email alias with corporate standards.
  • EasyEntra Template – a dropdown populated automatically from your existing EasyEntra templates at startup. Pick the right template for the role or department, and EasyEntra handles everything that flows from it: group memberships, licenses, mailbox provisioning and configuration, profile attributes, and more.

That’s it. Just a name and a template. Everything else in the onboarding process is driven by the template configuration you’ve already set up in AD or Entra ID.

Sample additional inputs in this form are:

  • Employee ID – the user’s internal employee number for HR and identity correlation.
  • Mobile Number – used for MFA and directory purposes.

By customizing the script, these values can be processed once the EasyEntra user creation script has executed.

Once you hit Create User, the system starts working in the background. You can see progress updates as each step runs. After the user is created, the script checks if everything was successful before running any additional steps.

Sometimes there is a small delay before the new user is fully available. The script handles this automatically by retrying until the user is ready.

The Result Object

One thing worth highlighting for anyone building this into a larger automation pipeline: Invoke-EECreateEntraUserFromTemplate (and its hybrid counterpart) don’t just run and disappear. They return a proper PSObject that you can inspect, parse, and act on programmatically.

Task Log

The real value is in $result.TaskLog, a structured log of every step the script executed:

$result.TaskLog 
Info      Initializing. 
Info      Getting AD Connection Info from EasyEntra. 
Info      Getting Entra Connection Info from EasyEntra. 
Info      Validating the EasyEntra license. 
Info      Resolving template user in Active Directory. 
Info      Preparing hybrid user creation from template. 
Info      Checking for conflicts. 
Info      Start user creation. 
Success   AD user creation completed. 
Info      Creating EXO mailbox. 
           > Mailbox type = UserMailbox 
Success   EXO mailbox creation completed. 
Info      Awaiting EXO/Entra ID convergence. 
Info      Start mailbox configuration. 
Success   Mailbox configuration completed. 
Info      Configuring Entra ID user. 
Info      Configuring licenses. 
Success   Entra ID user configuration completed. 
Completed User creation completed. 
EasyEntra user onboarding with PowerShell forms.
Example form using the EasyEntra script and running additional configuration scripts

Compliance and Audit Logging

The task log is ready to be written out as-is, to a flat file, a SQL database, a SIEM, or any system where your organization tracks provisioning events. You get a step-by-step record of exactly what happened, with clear status indicators at each stage. No need to instrument your own logging layer around it; EasyEntra has already done that work. Additionally, all actions are fully traceable in the tamper-safe Entra ID unified audit log.

Making It Your Own

The script in this post represents sample code you can modify to suit your individual needs:

  • Add or remove input fields in the form
  • Extend what happens after the user is created
  • Connect to external systems or APIs
  • Switch between cloud and hybrid provisioning with a small change

You can also remove the form entirely and turn it into a fully automated script if needed.

EasyEntra makes user onboarding much simpler by removing repetitive work and reducing errors. It gives you a strong base that handles most of the process automatically, while still allowing you to customize what matters to your organization. You can start with a simple form or go fully automated, driven by programmatic triggers. Either way, the process becomes faster, more reliable, and easier to manage.

In the end, it is about saving time, reducing mistakes, and making sure every new user is ready to go from day one.

Free 30-minute demo

try 30 days for free

GET EASYENTRA NEWS

Opt out at any time

“One of the best products I've used.”
Gary Shurland
Chief Information Officer, Mirick, United States
“This tool has been invaluable in streamlining our IT processes.”
Tyson Mckay
Chief Information Officer, Southwest Network, United States
“This product has been a miracle for our Help Desk. EasyEntra has completely transformed how we handle Microsoft 365 administration.”
Doug Sanders
Manager of Technical Customer Support, Junior Achievement USA, United States
“Your product is such a time saver. I love it!”
Scott Fehr
IT Infrastructure, MEC Aerial Work Platforms, United States
“It's a good product and saves us lots of time for these ongoing quick admin tasks.” 
Chris McFerran
Managing Director, CTech IT Solutions Ltd, United Kingdom
“EasyEntra has significantly streamlined our workflow, simplifying everything. It feels almost like a revolution.”
Johan Sadelius
IT-chef, Arjeplog Kommun, Sweden
I greatly appreciate your assistance and willingness to enhance the already outstanding product.”
Michael I. Wilson
Executive Director of Information Technology, Archdiocese Of Washington, United States
“It's great not having to switch back and forth between the O365 admin center and the Teams admin center to assign groups. I am sold!”
Thomas Madden
Director Information Technology, AutoPayPlus, United States
“I would highly recommend organizations use the solution as it greatly simplifies various tasks.”
S. Roger Singh
Chief Technology Officer, Prasad & Company LLP, Canada
“EasyEntra is time-saving. Love the copy/paste for user/computer groups and the copy to new user.”
Damian Nita
Associate Network Administrator, Shenandoah Valley Westminster-Canterbury, United States
“EasyEntra has transformed our daily IT operations by simplifying user management, reducing errors, and enhancing overall efficiency.”
Henrik Nefling
IT- and Digitalization Manager, Animal Protection Denmark, Denmark