In 2019, provisioning new users meant a quick script with a shared plaintext password,
run by hand and copied per client. In 2026, the same engineer directed an AI to rebuild
the same task for Microsoft Entra - and the prompt, the coding standards, and every
design decision are on display below, alongside the result.
The argument this page makes: AI output quality is a
function of the engineer directing it, and the direction is inspectable work
product. Everything here links to the public
demo repository.
Demo identities and domains are fictional, and the 2019 script's client domains were
fictionalized before publication; it is otherwise verbatim.
# the before
This is the 2019 script in full - 29 lines that created real accounts for real clients.
It works, which is exactly why it survived six years and six copies. The highlighted
lines are the problems the rebuild had to answer.
legacy/NewUsers.ps1 - verbatim, domains fictionalizedview in repo
The rebuild was not "AI, write me a script." Two artifacts sit in the repo beside the
code so the output quality is attributable to the direction, not to luck: the crafted
prompt, and the codified PowerShell standards the AI was told to follow.
prompt/prompt.md - the crafted prompt, in fullview in repo
# Prompt: modernize the new-user provisioning script
Follow my **pwsh-standards** skill for everything below.
I have a script from 2019, `legacy/NewUsers.ps1`, that reads a CSV and creates Active
Directory accounts on-prem. I want a modern replacement that provisions cloud identities in
Microsoft Entra instead. Build `src/New-EntraUsersFromCsv.ps1` to these requirements:
- **Microsoft Graph, not AD.** Use `New-MgUser` and the Graph module, not `New-ADUser`.
- **App-only certificate auth.** Connect with `Connect-MgGraph` using a tenant ID, client ID,
and certificate thumbprint. Never a password or a user account.
- **Config-driven by department.** Read a `department-map.psd1` that maps each department to an
M365 group and a license SKU. One script + one config replaces the six per-client copies I
used to hand-edit.
- **Group-based licensing.** Add the user to their department's group and let the license flow
from the group. Show the explicit per-user `Set-MgUserLicense` call as a commented
alternative, but do not use it.
- **No password, and the pass never travels by email.** Issue a one-time Temporary Access Pass,
write it to a per-hire Azure Key Vault secret, and email the manager only a pointer to that
secret (vault name, secret name, retrieval command) - never the pass value. Never write the
pass to a log, console, or email. Align both the pass and the secret's readable window to the
hire's start date (a StartDate column in the CSV): set the TAP's `startDateTime` and the
secret's `NotBefore`/`Expires` to the onboarding day with a matching workday-length window,
not a short lifetime from run time - accounts are provisioned early and the notification is
asynchronous, so a run-time countdown expires before the hire ever signs in, and a secret
readable early would just move the "credential sits around" problem from a mailbox to a vault.
- **Collision-safe and idempotent.** First-initial + last name; if `jdoe` is taken, use `jdoe2`.
Check the tenant on a real run; dedupe within the batch on a dry run.
- **Safe by default.** `SupportsShouldProcess`; a full `-WhatIf` that connects to nothing,
changes nothing, and prints the plan. It must run on a machine with no Graph module installed.
- **Written for a human reviewer.** Full cmdlet and parameter names, no aliases, splatting,
a guiding comment before each block. Someone should be able to read it without running it.
Handle bad input without crashing the batch: skip rows missing required fields or with an
unmapped department, report why, and keep going.
standards/pwsh-standards.SKILL.md - excerpt: the section this build addedview in repo
## Written for a Human Reviewer
Scripts run unattended far more often than a human reads them, but a reviewer who cannot
follow the script will not trust it, and an untrusted script does not get approved.
Optimize generated PowerShell for a human reading it top to bottom:
- **No aliases.** Use `Where-Object` not `?`, `ForEach-Object` not `%`, `Get-ChildItem` not
`gci`/`ls`/`dir`, `Select-Object` not `select`. Aliases are for the interactive prompt,
never for saved scripts.
- **Full parameter names, no positional arguments.** Write `Get-MgUser -UserId $upn`, not
`Get-MgUser $upn`. The reader should never have to remember positional order.
- **Splat multi-parameter calls** into a hashtable rather than using backtick
line-continuation; splatting reads cleanly and avoids trailing-backtick breakage.
- **A guiding comment before each logical block** stating what the block accomplishes and why.
The full standards file covers script structure, naming, parameter validation, error
handling, and Graph authentication patterns. The
commit
history shows the build proceeding task by task against those standards.
# the nine decisions
The design was built one decision at a time, each challenged before it was accepted.
The sequence is the actual skill on display. Expand each one for the reasoning.
# before and after, by concern
Seven concerns, side by side. Excerpts are verbatim from the published repo; where the
2019 script simply has no equivalent, the honest answer is written instead of code.
# the terminal
This is the rebuild's offline dry run - the real output, captured by a script in this
site's repo, replayed here. It connects to nothing and changes nothing. Watch for the
collision handler and the skipped rows.
pwsh - offline dry run (-WhatIf)
jdoe then jdoe2: two J. Does in one batch; the collision
handler resolved the second automatically.
Two rows SKIPPED with a stated reason each - bad input is reported, never fatal.
Seven rows Planned (WhatIf): the full provisioning plan, with zero
changes made.
# run it yourself
The dry run needs PowerShell 7 and nothing else: no tenant, no Graph module, nothing
to clean up.