one task, seven years apart

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.

$UserList = Import-Csv -Path 'C:\users\mwhitman\Documents\Scripts\NewUsers.csv'

foreach ($User in $UserList) {

     $Attributes = @{

        Enabled = $true
        ChangePasswordAtLogon = $true
        Path = "OU=$($User.OU),DC=contoso,DC=local"

        Name = "$($User.First) $($User.Last)"
        DisplayName = "$($User.First) $($User.Last)"
        UserPrincipalName = "$(($User.First).substring(0,1))$($User.Last)@contoso.local"
        SamAccountName = "$(($User.First).substring(0,1))$($User.Last)"
        EmailAddress = "$(($User.First).substring(0,1))$($User.Last)@contoso.com"

        GivenName = $User.First
        Surname = $User.Last

        Company = $User.Company
        Department = $User.Department
        AccountPassword = "Summer2019!" | ConvertTo-SecureString -AsPlainText -Force
        OtherAttributes = @{proxyAddresses = ("SMTP:" + "$(($User.First).substring(0,1))$($User.Last)@contoso.com"), ("smtp:" + "$(($User.First).substring(0,1))$($User.Last)@contosoclinic.com")}

     }

    New-ADUser @Attributes

}

    chain of evidence

    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 full view 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.** Issue a one-time Temporary Access Pass and email it, with first-sign-in
      instructions, to the hire's manager (a Manager column in the CSV) for in-person handoff.
      Never write the pass to a log. Align the pass to the hire's start date (a StartDate column
      in the CSV): set its `startDateTime` to the onboarding day with a workday-length window,
      not a short lifetime from run time - accounts are provisioned early and the email is
      asynchronous, so a run-time countdown expires before the hire ever signs in.
    - **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 added view in repo
    ## Written for a Human Reviewer
    
    Scripts are read more than they are run, and a reviewer who cannot follow a script will not
    trust it. 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 eight 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

    Six 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)
    
          

    run it yourself

    The dry run needs PowerShell 7 and nothing else: no tenant, no Graph module, nothing to clean up.

    git clone https://github.com/thoumyvision/entra-provisioning-demo.git
    cd entra-provisioning-demo
    pwsh -File src/New-EntraUsersFromCsv.ps1 -CsvPath data/new-hires.csv -WhatIf