Field notes · automation
HR-driven provisioning: hires and exits as data, not email
IT is always the last to know. How I wired hires and departures to the HR system with a 20-line export diff, and why the trigger beats any script.
Every post in this identity-lifecycle set automates a step, and every one of them dodges the same upstream question: how did IT find out in the first place? The onboarding system starts when a manager submits the intake form. The offboarding system starts when someone tells me a person left. Both starts depend on a human remembering to involve IT, and the departure side barely works at all: more than once I learned someone was gone only when their manager asked for access to the mailbox.
HR-driven provisioning closes that gap. The system of record is the system where a fact becomes officially true, and for hires and departures that is never IT. It is the HR or payroll system, updated on time, every time, because paychecks depend on it. The trigger belongs there. If a paycheck starts, IT should know. If a paycheck stops, IT should really know.
Key takeaways
- The intake form fixed the format of requests. It did nothing for the notifications nobody sends.
- HR’s system is the system of record: it is updated reliably because pay depends on it. Hang the trigger on it.
- A scheduled diff of an HR export against the directory catches both hires and departures with about 20 lines of code.
- Match on an employee ID, never on names. Names change, repeat, and get re-spelled.
- The diff creates requests for human approval. It never provisions or disables anything by itself.
Why is the trigger worth more than the script?
Because a script that never fires might as well not exist, and the failure mode is invisible. A provisioning script with a bug fails loudly on the account you are creating. A missing trigger fails silently on the account you never knew to create, or worse, never knew to disable. That asymmetry is the entire reason the offboarding posts keep circling back to “finding out.”
The manager-facing form works well for hires because a manager wants the new hire working. Nobody has the same urgency about a departure. The person who reliably knows about both events is HR, and HR already records them in a system, on a deadline, with an employee ID attached. The trigger problem is not “build a notification habit.” It is “read the system where the fact already lives.”
What are the options for HR-driven provisioning?
Three tiers, in ascending order of plumbing. I use the middle one.
| Approach | Effort | Latency | Where it fails |
|---|---|---|---|
| Process hook: HR’s checklist includes notifying IT | None | Whenever someone remembers | Relies on the habit it replaces |
| Scheduled export diff against the directory | An afternoon | One day | Bad or missing employee IDs |
| API or webhook from the HR platform | Days, if the vendor allows it | Minutes | The API that was never built for you |
The process hook is better than nothing and is where everyone starts. The API tier is where the big shops live, and if your HR platform has a usable API, take it. The middle tier is the small-shop sweet spot: almost every HR or payroll system can produce a scheduled export of active employees, and a daily diff of that export against your directory catches everything the hallway misses.
How does the export diff work?
Compare two lists by employee ID and route the differences into the same request queue the intake form feeds. In the export but not the directory: a hire candidate. In the directory but not the export: a departure candidate. Twenty lines does it, with Compare-Object doing the actual work.
# HR export vs. directory, matched on employee ID. Differences become
# requests for review, never direct provisioning actions.
$hr = Import-Csv $hrExport | Where-Object { $_.Status -eq 'Active' }
$ad = Get-ADUser -Filter { Enabled -eq $true } -Properties employeeID |
Where-Object { $_.employeeID }
$diff = Compare-Object -ReferenceObject $hr.EmployeeId `
-DifferenceObject $ad.employeeID
foreach ($d in $diff) {
$kind = if ($d.SideIndicator -eq '<=') { 'HIRE-CANDIDATE' } else { 'DEPARTURE-CANDIDATE' }
New-LifecycleRequest -Type $kind -EmployeeId $d.InputObject
}
Two design choices carry the weight. First, the output is a request, not an action: a hire candidate lands in the same approval queue as a form submission, and a departure candidate opens a departure ticket for me to confirm. The diff has no authority. It has attention. Second, everything keys on the employee ID, which brings us to the prerequisite.
Why match on employee ID instead of names?
Because names are the least stable identifier an organization has. People marry, divorce, go by middle names, and get typed differently by HR and IT in the same week. My first diff matched on last name. How bad could that be? The HR export said “Peggy,” the directory said “Margaret,” and the diff drowned me in hires and departures that did not exist.
The fix is boring plumbing: the HR employee ID goes into the directory’s employeeID attribute at provisioning time, and the intake form collects it for that purpose. Backfilling it onto existing accounts was an afternoon of matching by hand, once. Reconciliation is that one-time cleanup where the two systems agree on who is who; after it, the diff runs on IDs and the false positives stop.
What still stays human?
The confirmation, on both sides. A hire candidate still goes through the same approval glance as a form submission, because HR data has its own quirks: contractors who should not get accounts, rehires who already have one, a future start date entered early. A departure candidate absolutely gets a human look before anything is disabled. “Not in the active export” sometimes means unpaid leave, a data entry slip, or a seasonal gap, and disabling a live employee is a bad morning.
The diff also does not replace the intake form. The form still carries what HR does not know: which manager, which role details, day-one equipment, the notes field. The diff is a safety net under the form for hires, and it is the primary trigger for departures, where no form was ever coming.
What broke, and what I would change
Status values surprised me. “Active” in the HR export turned out to include people on certain kinds of leave, and one flavor of “Terminated” was really a transfer between departments. I learned to sit with HR for half an hour and walk through every status code before trusting the export. I now re-check whenever the diff produces a candidate that smells wrong, because the status vocabulary changes without a memo.
The other lesson: run the diff daily even though it feels like overkill. I started weekly, and a departure sat unknown for six days, which is exactly the window the access-revocation post exists to close. The report takes seconds. The gap it closes is measured in days of a former employee holding live access.
FAQ
How does IT find out someone was hired or left without being told?
By diffing the HR system’s active-employee export against the directory on a schedule, matched on employee ID. A person in the export but not the directory is a hire candidate; a person in the directory but not the export is a departure candidate. Both become tickets, not automatic actions.
What if the HR system has no API?
You almost never need one. A scheduled CSV export of active employees, which nearly every HR or payroll platform can produce, plus a daily Compare-Object diff, delivers most of the value of an integration at a fraction of the plumbing.
Should the HR diff disable accounts automatically?
No. A missing row can mean leave, a data correction, or a seasonal gap, not just a departure. Let the diff open the departure ticket and let a human confirm before the day-of-departure script runs. The automation’s job is making sure you find out, not acting on it blind.
What has to be true before an HR-to-directory diff works?
A shared identifier. Put the HR employee ID in the directory’s employeeID attribute at provisioning time, and reconcile existing accounts once by hand. Name matching produces false hires and false departures as soon as a preferred name or a re-spelling shows up.