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.
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 built onboarding into a small web form plus a PowerShell script, what it saved me, what I learned the hard way, and the parts I deliberately left manual. Provisioning is the fulfillment half of onboarding: turning one approved request into a fully configured account.
Onboarding a person touches more than the directory, though. It reaches into email, the HR record, the badge, and the handful of outside systems each department lives in. This post is the core of it, creating the account itself. The other pieces each cost me enough to earn their own write-up, so I am publishing them as a set, and there is a map of the whole system near the end.
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.
Each of those failures maps to a specific part of the system that fixes it:
| Manual failure | What the system does |
|---|---|
| Inconsistent group assignment | A department-to-groups map |
| Missing or dirty inputs | A validated web form |
| No record of what was done | A provisioning log |
| Interrupts real work | The manager self-serves the request |
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:
- 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.
- 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.
The uniqueness helper
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 using
Get-ADUser:
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
}
The department-to-groups map
Next, a department-to-groups map. A department-to-groups map is a lookup table that pairs each department with the groups its people need. This is the part that used to live in my head, and moving it into 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
}
Creating the account
Now the account creation itself, with
New-ADUser.
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. The first is a
Write-ProvisionLog function that appends a timestamped line to a log file. This
is the record I never had before, and it earns its keep the first time someone asks
about an account months later. The second is an idempotency check. An idempotency
check is a guard that makes re-running a partial request safe, so it never creates
a duplicate account or errors 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. I found that 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 rest of the onboarding system
Creating the account is the core, but it is one stop on a longer line. Here is the whole system the way I actually run it, and where each piece is going as its own post, because each one has enough that bit me to deserve the space.
- Capturing the request. The manager-facing intake form and the approval gate, the clean-inputs half this post leans on but does not build.
- Creating the account. The Active Directory account and its group membership. This post.
- Microsoft 365. Assigning the license, waiting out the mailbox before the welcome steps, and adding the person to the right Teams. The timing is the trap.
- The trigger. Wiring the whole thing to fire from the system HR already updates, so a new hire reaches you as data instead of as a hallway conversation.
- The rest of the stack. Every small shop bolts on a few outside systems: a workforce platform, an email-security layer, a badge and door-access system. Each is another account to create, usually through an API that was never built for you.
I started with account creation because it is the step you repeat most and the one a mistake follows around the longest. The others build outward from it.
The takeaway
You do not need an enterprise identity platform to make onboarding sane. You need to do three unglamorous things:
- Capture clean, structured inputs so you stop chasing missing fields.
- 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.
- 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.
Onboarding is only half of the identity lifecycle. The other half, the one that fails silently instead of loudly, is the departure. I covered it in automating offboarding in a one-person IT shop, which is the mirror image of this post: same idea, reversed, with the security and license-reclaim stakes that make it easy to forget and expensive to skip.