Field notes · powershell

Automating new-hire onboarding in a one-person IT shop

How I replaced a manual, error-prone new-hire checklist with a web form and a PowerShell provisioning script, plus the gotchas that made me keep a human in the loop.

·7 min read

If you run IT for a small organization, you know the new-hire drill. A manager emails you (or worse, catches you in the hallway) two days before someone starts. You create an Active Directory account, add them to the right groups, set up a mailbox, make a home folder, and hope you remembered everything the last person in that role had. Do it enough times and you develop a checklist. Do it a few more times and you realize the checklist is the actual system, and you are just a slow, error-prone interpreter of it.

I got tired of being the interpreter. Here is how I turned onboarding into a small web form plus a PowerShell script, what it saved me, and the parts I deliberately left manual.

Key takeaways

  • The bottleneck in small-shop onboarding is not running commands, it is collecting clean inputs and keeping a record. Solve those first.
  • A web form for the hiring manager plus a provisioning script beats one big script you run by hand, because it removes the interrupt and the email back-and-forth.
  • Move your mental checklist into data (a department-to-groups map) so the machine is consistent where you are not.
  • Keep a human approval step. A five-second glance catches the mistakes a fully automated pipeline would happily execute.

Why automate onboarding at all?

Because manual onboarding fails in boring, repeatable ways, and those failures cost you at the worst possible time. Here is what actually went wrong when I did it by hand:

  • Inconsistency. Two people hired into the same role would end up in different groups because I was going from memory.
  • Bad inputs. “Bob in Finance” is not enough to make an account. What is the legal first name? Preferred name? Start date? Manager? I would end up in three email threads before I could type a single command.
  • No record. When someone asked six months later why a user had access to a share, I had nothing but my own recollection.
  • It always happened at the worst time. Onboarding is bursty and urgent, and it interrupts whatever real project I was in the middle of.

The checklist solved the “what to do” problem. It did nothing for the “get clean inputs” and “keep a record” problems, and those were the ones actually hurting.

What does a small-shop onboarding system actually need?

Two things, split apart: a structured way to capture the request, and a reliable way to fulfill it. Keeping them separate is the whole trick.

I started with one big PowerShell script I ran by hand. It helped, but I still had to hand-collect the inputs and hand-run the command, so the interrupt cost was unchanged. What actually moved the needle was splitting the job in two:

  1. A web form the hiring manager fills out. It captures structured inputs (legal name, preferred name, department, role, manager, start date) with validation, so I never chase a missing field again.
  2. A provisioning script that turns one approved submission into a fully configured account.

I rejected a few tempting options. A full identity-management product was overkill and over budget for our size. Wiring the form straight to the script with no human step felt fast but reckless, because a typo in a manager field or a duplicate name should be caught by a person before an account exists. So I kept an approval gate in the middle: the form creates a request, I glance at it and approve, and only then does provisioning run.

How do you build it in PowerShell?

Start with the two failure points that bite hardest: username collisions and the group assignments you keep in your head. Solve those in code and the rest is routine. The real version reads its mapping and OU paths from a config file, but the shape is what matters.

First, a helper to generate a unique account name. Username collisions are the single most common way naive onboarding scripts blow up, so this gets its own function with a real uniqueness check against the directory:

function Get-UniqueSamAccountName {
    param(
        [Parameter(Mandatory)] [string] $FirstName,
        [Parameter(Mandatory)] [string] $LastName
    )

    # Base pattern: first initial + last name, lowercased, ASCII only.
    $base = ('{0}{1}' -f $FirstName.Substring(0,1), $LastName).ToLower()
    $base = $base -replace '[^a-z0-9]', ''

    $candidate = $base
    $suffix = 1
    while (Get-ADUser -Filter "SamAccountName -eq '$candidate'" -ErrorAction SilentlyContinue) {
        $suffix++
        $candidate = '{0}{1}' -f $base, $suffix
    }
    return $candidate
}

Next, a department-to-groups map. This is the part that used to live in my head. Putting it in a data structure is most of the value: it turns “what groups does a Finance hire need” from a memory test into a lookup.

$DepartmentGroups = @{
    'Finance'    = @('Finance-Staff', 'ERP-Users', 'Shared-Finance')
    'Clerk'      = @('Clerk-Staff', 'Records-Users')
    'PublicWorks'= @('DPW-Staff', 'GIS-Viewers')
    # ...one entry per department
}

Now the account creation itself. A few things I consider non-negotiable here: splatting so the call stays readable, a password delivered as a SecureString, and -ErrorAction Stop inside a try/catch so a failure is loud instead of silent:

function New-OnboardedUser {
    param(
        [Parameter(Mandatory)] [pscustomobject] $Request
    )

    $sam = Get-UniqueSamAccountName -FirstName $Request.FirstName -LastName $Request.LastName
    $upn = '{0}@example.com' -f $sam
    $password = New-RandomPassword   # see the gotcha below

    $userParams = @{
        Name              = '{0} {1}' -f $Request.PreferredName, $Request.LastName
        GivenName         = $Request.FirstName
        Surname           = $Request.LastName
        DisplayName       = '{0} {1}' -f $Request.PreferredName, $Request.LastName
        SamAccountName    = $sam
        UserPrincipalName = $upn
        Department        = $Request.Department
        Title             = $Request.Role
        Manager           = $Request.ManagerSam
        Path              = 'OU=Staff,OU=Users,DC=example,DC=local'
        AccountPassword   = $password
        ChangePasswordAtLogon = $true
        Enabled           = $true
    }

    try {
        New-ADUser @userParams -ErrorAction Stop

        foreach ($group in $DepartmentGroups[$Request.Department]) {
            Add-ADGroupMember -Identity $group -Members $sam -ErrorAction Stop
        }

        Write-ProvisionLog -Level 'INFO' -Message "Created $sam for $($Request.Department)"
        return [pscustomobject]@{ Sam = $sam; Upn = $upn }
    }
    catch {
        Write-ProvisionLog -Level 'ERROR' -Message "Failed to create $sam: $($_.Exception.Message)"
        throw
    }
}

Two supporting pieces I will not show in full but that matter: a Write-ProvisionLog function that appends a timestamped line to a log file (this is the record I never had before, and it is worth its weight the first time someone asks about an account months later), and an idempotency check so re-running a request that partially completed does not create a duplicate or error out on the group adds.

What breaks, and what I changed

Mostly the things you would not predict until they bite you: name collisions, generated passwords, and mailbox timing. Each one cost me real time, so here is what happened and what I did about it.

Username collisions were worse than I expected. Two people with the same first initial and last name is not rare once you have any headcount. The uniqueness loop above exists because my first version happily tried to create a duplicate and threw a confusing error. Checking the directory before committing to a name fixed it.

Generated passwords broke downstream tools. My first password generator used the full symbol set. Some of those characters (&, %, +, spaces, quotes) break when a password lands in a URL, a connection string, or a script argument somewhere else in the pipeline. I now exclude the characters that cause trouble in shells and URLs, keep the length up to preserve strength, and I flag any generated secret for the handful of symbols that still bite. That one cost me an afternoon of “why does this new user’s automated login fail.”

Mailbox timing is not instant. Enabling a mailbox and having it actually be usable are not the same moment. I stopped trying to do everything in one synchronous run and let the mail side settle before the welcome steps.

The approval gate earned its keep immediately. The first week, a manager submitted a request with themselves listed as the new hire’s manager by mistake, and a start date in the past. A fully automated pipeline would have created the account anyway. Because a human glances at each request, both got caught in seconds. Speed is good, but a human veto on account creation is cheap insurance.

Frequently asked questions

Can this run fully unattended, with no human approval?

Technically yes, and I would still advise against it for account creation. The approval step costs you a few seconds and catches the class of error no validation rule anticipates: a manager field pointing at the wrong person, a nonsense start date, a duplicate hire. Automate the work; keep the veto.

What about systems beyond Active Directory, like email or a badge system?

Treat each as its own step the provisioning script calls after the account exists, and make each step idempotent so a partial run is safe to repeat. The mailbox is usually the touchy one because of timing, so I let it settle rather than forcing it into the same synchronous pass as the account creation.

How do you handle a rehire or a name change?

That is exactly why the uniqueness check queries the directory instead of trusting a formula, and why the run is idempotent. A rehire surfaces as an existing account you decide to reactivate rather than recreate, and a name change becomes an update path rather than a brand-new object. Log both, so the history is clear later.

The takeaway

You do not need an enterprise identity platform to make onboarding sane. You need to do three unglamorous things:

  1. Capture clean, structured inputs so you stop chasing missing fields.
  2. Move your checklist out of your head and into data (the department-to-group map) so the machine can be consistent where you were not.
  3. Log everything and keep a human approval so you have a record and a veto.

Automate the boring, repetitive 80 percent. Keep judgment where judgment belongs. The goal is not to remove yourself from onboarding, it is to stop being the part of it that forgets things at 4:45 on a Friday.