Plugins are the only place source data enters IDBridge. They live outside this repo
in C:\IDBridge\Plugins\ (= Paths.PluginsRoot) and are registered in the Plugins array
of IDBridgeConfig.psd1. Each plugin is a single .ps1 file
whose function name matches both the config Function value and the file name.
Sanitized templates of the shipped plugins are packaged with the module
(Templates\Plugins\) and copied into PluginsRoot by Install-IDBridge (existing
files are never overwritten; re-run it after a module update to pick up newly shipped
templates). Templates with placeholder values (spreadsheet ID / API URLs, domain, OUs)
throw until they are edited; the PostRun report and export templates work as-is. The
worked examples below describe the full deployment versions the templates were derived
from.
Every template starts with a # TemplateVersion: <n> marker — leave that line in your
copy. When a shipped template changes, its version is bumped, and Install-IDBridge
compares the marker in your installed copy against the shipped one and prints a
"newer template available" notice on re-run, pointing at the shipped file to review.
Your edited copy is never modified; port changes by hand.
There are three kinds:
- Source — produce the canonical list of people to manage (one record per person).
- Override — modify already-loaded source records, matched by
personID. - PostRun — consume the finished run's results (reports, dashboards, your own telemetry). See the PostRun contract.
Source of truth: src\IDBridge\Private\Source\Invoke-SourcePlugins.ps1.
For each enabled Plugins entry, in config order (PostRun entries are skipped here —
they run at end of run):
- Skip if
Enabled -ne $true. - If
<PluginsRoot>\<Function>.ps1exists, dot-source it (load failure ⇒ warn + disable). A missing file is tolerated when the function already exists in the session — theGet-Commandcheck below is the real gate. - Require
Get-Command <Function>to resolve (missing ⇒ warn + disable). - Invoke with no arguments:
& $plugin.Function. A Source/Override plugin that throws aborts the whole run (unlike PostRun plugins, which are isolated — see the PostRun contract below). - Append the returned object(s) to
SourceDataorOverrideDatabased onType. WhenDebug.testRunis set, each source plugin's validated output is capped at the first 10 records before collection (afterTest-IDBridgeSourceDataand the plugin's own safety floors) — plugins don't need to implement test-run logic themselves.
Returns [PSCustomObject]@{ SourceData; OverrideData }. Throws if SourceData is empty
after all plugins run. A plugin therefore:
- takes no parameters,
- pulls everything it needs from
Get-IDBridgeConfig(paths, secrets, debug flags), - logs via
Write-Log, - returns an array of
[PSCustomObject](or$null/empty for an override that has nothing).
Plugins may also define helper functions in the same file (e.g.
Get-CustomStaffGroups,Get-CustomStudentGroups). Because the file is dot-sourced, those helpers become available in the session; the plugin calls them guarded byTest-Path Function:\<Name>so they're optional.
Source plugins must build each record with the New-IDBridgeSourceRecord factory (splat
your field hashtable: New-IDBridgeSourceRecord @recordFields) rather than hand-rolling a
PSCustomObject. The factory guarantees one canonical, ordered shape and enforces required
fields + types at construction (core fields are Mandatory; PersonTypeID is a
ValidateSet; IDBActive/ProvisionAD/ProvisionGoogle/ProcessGroupsAD/ProcessGroupsGoogle
are [bool]; GroupsProposed null is normalized to @()). Downstream Get-*To* functions
depend on these exact property names:
| Property | Notes |
|---|---|
PersonID |
Universal join key (→ AD EmployeeID, Google externalId). |
NameFirst, NameLast, Username |
Identity basics. |
Building, JobTitle, Company, Department |
Org attributes (Building→AD Office/Google dept). |
UPN |
Primary email / SamAccountName source, e.g. username@domain. |
PersonTypeID |
"1" = student, "2"/"3" = staff tiers. Drives the staff-CSV export filter. |
InternalID |
Optional secondary id (AD EmployeeNumber). |
GroupsProposed |
[string[]] of desired group names (de-duplicate in the plugin — the factory doesn't). |
PersonType |
Human-readable role (used for OU paths + custom-group logic). |
Description, TelephoneNumber, EmailAddress |
Optional AD attributes (Description / OfficePhone / mail). Applied only when non-empty (set-but-don't-clear). |
PasswordNeverExpires |
[bool] (def $false) — AD passwordNeverExpires. |
ExtensionAttribute2, ExtensionAttribute3, ExtensionAttribute4 |
Optional AD extensionAttributes (1 is set from PersonTypeID); reserved for future use. |
ForceDisable |
[bool] (def $false) — override-driven force-disable/suspend. |
GoogleOUOverride |
[bool] (def $false) — override-driven skip of the Google OU move. |
IDBActive |
Master "active in source" flag. $false ⇒ deactivate in every directory. |
ProvisionAD / ProvisionGoogle |
Per-user directory targeting [bool]. With IDBActive: create/update when both true; deactivate when either is false. (A young student = ProvisionAD=$false.) |
ProcessGroupsAD / ProcessGroupsGoogle |
Per-user group-processing scope [bool] (def $true). $false leaves that directory's group memberships untouched for the person — no adds, no removes, no deactivate strip. Only narrows the directory's enableGroupProcessing; it never turns group processing on. This is how group processing is enabled per source type (staff vs. students) or per grade. |
ADOrganizationalUnit / GoogleOrganizationalUnit |
Target OU (DN / path). |
ADOrganizationalUnitTrash / GoogleOrganizationalUnitTrash |
Deactivation OU. |
ADChangePasswordAtLogon / GoogleChangePasswordAtLogon |
Force change flag. |
ADPassphraseAPI / GooglePassphraseAPI |
@{Nonce;Mode;WordCount;AuthToken} or $null (→ New-Passphrase). |
ADKey / GoogleKey |
SecureString password, or $null if using the passphrase API. |
Password selection: a *PasswordType of API-PASSPHRASE populates *PassphraseAPI (and
leaves *Key null); WORD/FSPIN/RANDOM populate *Key as a SecureString. A record with
neither a key nor passphrase config is skipped by the create functions (logged Warn).
Invoke-SourcePlugins runs every source plugin's output through Test-IDBridgeSourceData
before collecting it. Validation is filter-and-log: a record that fails is dropped with a
Warn (plugin name + reasons) and the run continues with the rest. Checks are the cross-field
rules the factory can't enforce on its own:
PersonIDpresent andIDBActiveis a real boolean (safety net for records that bypass the factory);- if
ProvisionAD→ADOrganizationalUnit+ADOrganizationalUnitTrashnon-empty and (ADKeyis a SecureString orADPassphraseAPIis a hashtable); - same for the
Google*fields.
Building records through New-IDBridgeSourceRecord makes most of this pass automatically.
Override plugins are not run through this — they self-validate (see
Invoke-PluginStaffOverride).
Each record needs PersonID plus one or more override keys; all unset keys should be $null.
Applied by Merge-IDBridgeOverrideData.
| Key | Effect |
|---|---|
NameFirst, NameLast, Username |
Overwrite the matching source field. |
ForceDisable |
$true ⇒ force the account suspended/disabled even if active. |
GoogleOUOverride |
$true ⇒ leave the user's Google OU untouched (skip OU move). |
AddGroup |
Append a group name to GroupsProposed. |
RemoveGroup |
Remove a group name from GroupsProposed. |
Override merge skips PersonID itself and any null/blank value, so a sparse override row only
touches the fields it sets.
Source of truth: src\IDBridge\Private\Core\Invoke-PostRunPlugins.ps1.
PostRun plugins run from the finally block of Invoke-IDBridge, after telemetry and
before the Google Sheet log push (so their Write-Log lines make it into the sheet). They
fire on every run — failed and ReadOnly runs included; the RunResult carries
Success/ReadOnly so the plugin decides what to do. The one exception is
Invoke-IDBridge -Preview: preview runs stay quiet and skip telemetry, PostRun plugins,
and the log-sheet push entirely. Discovery/loading is identical to
the source contract (file check, dot-source, Get-Command, warn + disable on failure),
but each plugin:
- is invoked as
& <Function> -RunResult <RunResult>— so it declaresparam([pscustomobject]$RunResult), - is isolated in its own try/catch: a throw is logged as a
Warnand the remaining plugins still run — a PostRun plugin can never fail the run or mask its outcome, - returns nothing (any output is discarded),
- pulls the config from
Get-IDBridgeConfig(it already reflects the run's switch overrides) and the run's log lines fromGet-IDBridgeLogs, same as source plugins — neither is duplicated on the RunResult.
One [pscustomobject], grown additively only — new properties may appear, existing
ones won't be renamed. Check SchemaVersion if you depend on shape.
| Property | Notes |
|---|---|
SchemaVersion |
1. |
ModuleVersion |
The IDBridge module version that produced the run. |
Success |
$false when the run threw a fatal error. |
RunError |
The full ErrorRecord of a failed run (message + stack — nothing is stripped locally), or $null. |
RunStart / RunEnd / DurationSeconds |
Run timing. |
ReadOnly / TestRun |
Effective mode flags after switch overrides — interpret zero counts with these. |
Counts |
Managed/Create/Update/Deactivate/GroupAdd/GroupRemove/Failed — actual applied outcomes aggregated from Applied (Update includes renames/moves; Failed = total failed writes). Counts are directory writes, not people: a person provisioned to both AD and Google counts once per directory (Managed is the per-person number). Zeros in ReadOnly or while a directory's group processing is off/WhatIf — nothing attempted, nothing recorded. |
Applied |
One record per attempted write: Timestamp/Directory/Action/PersonID/Target/Success/Error (Action ∈ Create, Update, Rename, Move, Deactivate, GroupAdd, GroupRemove; Error $null on success). Empty in ReadOnly. |
SourceData |
The full enriched source records (incl. matched ADObject/GoogleObject where found). |
ThresholdResults |
Change-volume guard results (Directory/ChangeCount/PopulationCount/Percent/Exceeded/Skipped per enabled directory; Percent is $null when skipped), empty if the guard is off. |
AD |
Enabled, UsersToCreate, UsersToUpdate (keeps its UpdateList/RenameList/MoveList shape), UsersToDeactivate, GroupsToUpdate (keeps its Add/Remove shape), OrgUnitsToCreate, CurrentUsers (the full fetched AD user list the run matched against). |
Google |
Same property names: Enabled/UsersToCreate/UsersToUpdate/UsersToDeactivate/GroupsToUpdate/OrgUnitsToCreate/CurrentUsers — but Google.UsersToUpdate is a flat array (no UpdateList/RenameList/MoveList sub-lists; only the AD one has those). |
On a failed run the lists that were never computed are empty arrays (UsersToUpdate/
GroupsToUpdate may be $null) — guard accordingly.
Two things to know about the data:
- SecureStrings are scrubbed. Every SecureString anywhere in the graph (
ADKey/GoogleKey, passphrase-APINonce/AuthToken, create-splat passwords) is replaced with$nullbefore any plugin sees the RunResult. Everything else — names, IDs, UPNs, group names — is intact: it's your data on your server, but think before shipping records off the box; a webhook payload built from counts can't leak a student record. - The change lists are computed work;
Appliedis what actually happened. TheAD/Googlelists are what the run intended;Appliedrecords the per-write outcome ("did user 1001's update stick?") — filter onSuccess -eq $falsefor a failure report (the shipped report template does this). Two write types are not covered byAppliedand remain log-only: Google license removals and OU creation (an OU failure aborts the run anyway).
File: C:\IDBridge\Plugins\Invoke-PluginGSheetStaff.ps1. Pulls staff from a Google Sheet via
Get-SourceDataGSheet (spreadsheet ID + range Staff, safety floor 650 @ 75%).
UPN = <Username>@yourdistrict.org; AD OUOU=<PersonType>,OU=Staff,<root>, Google OU/YourDistrict/Staff/<PersonType>; trash OUs use the current year.PersonTypeID = "3"whenPersonTypeis in a leadership/role list (Administrator, Teacher, Principal, …), else"2".IDBActive = $falsewhenTerminationDateis in the past.- AD password type
WORD(prefixTemp-+ sheetWord), GoogleRANDOM(GUID). Passphrase API only if a type isAPI-PASSPHRASE(reads theApiKey-PassphraseNonceStaff+ApiKey-Passphrasevault secrets viaGet-IDBridgeSecret). - Groups =
Get-CustomStaffGroups -building -personType(optional, bundled in-file) + comma-splitApplicationGroups+EmailGroups, de-duplicated. The bundled helper is a starting point (All Staff,<building> Staff, andAll Professional Staffvia a person-type allow-list) — extend it to encode your district's group policy. - Group-processing scope:
$ADProcessGroups/$GoogleProcessGroups(default$true) setProcessGroupsAD/ProcessGroupsGoogleon every staff record — set one$falseto leave staff group memberships untouched in that directory while students are still processed.
File: C:\IDBridge\Plugins\Invoke-PluginStaffOverride.ps1. Reads Get-GoogleSheetData
(same sheet, range Override). Required columns: PersonID, Type, Value, StartDate, EndDate.
- Validates
Typeagainst the allowed override list; invalid ⇒ Warn + skip. - Date-gated: skips rows whose
StartDateis future orEndDateis past, so overrides self-activate/expire. - Emits one normalized record per row with the matched
Type=Value(andForceDisable/GoogleOUOverridecoerced to$true).
File: C:\IDBridge\Plugins\Invoke-PluginSkywardSMSStudents.ps1. Pulls students via
Get-SourceDataSkywardSMS (OneRoster API; client secret from the vault secret
ApiKey-SkywardSMS; exclude entity 800; safety floor 3700 @ 75%).
- Pads
FoodServiceKeyPadNumberto 4 digits when present (used as the FSPIN password); an optional commented-out filter drops students without a PIN when a grade uses FSPIN. PersonID/Username = DisplayId,InternalID = NameId,UPN = <DisplayId>@my.yourdistrict.org,PersonTypeID = "1".- Grade from
GradeLevel; building from aSchoolID→name/code map (fallback000). - Per-grade settings (
GradeDefaultSettings+GradeOverrides, merged acrossValidGrades) control provisioning and AD/Google password type/prefix/change-flag. AD OUOU=Grade-<grade>,OU=Students,<root>, Google/YourDistrict/Students/Grade-<grade>. - Per-directory provisioning:
ProvisionAD = [bool]$GradeSettings.<grade>.AD.ProvisionandProvisionGoogle = [bool]…Google.Provision— so "younger grades = Google only" is set by adding aGradeOverridesentry withAD = @{ Provision = $false }. - Per-directory group scope:
ProcessGroupsAD/Google = <grade>.AD/Google.ProcessGroups -ne $false(default$true) — so students can be left out of a directory's group processing entirely (set it inGradeDefaultSettings) or per grade ('01' = @{ AD = @{ ProcessGroups = $false } }). Read as-ne $falserather than[bool]so a settings block without the key stays opted in. - Name casing:
NameFirst/NameLastare run through the module'sFormat-IDBridgeName(Skyward returns ALL-CAPS → Title Case). Because the update functions compare names case-sensitively (-cne), existing accounts get the casing fix too, not just new ones. IDBActive: false if not seen withinDaysLastSeen(14), or if the grade is missing/ disabled in settings. Graduated (GD) students stay provisioned and active until$GraduateActiveUntil(default09/01) of theirGradYr, then deactivate — disable GD provisioning inGradeOverridesto deactivate them immediately instead.- Groups =
Get-CustomStudentGroups -building -grade(bundled:Students, optional<code>_Students,Grade-<grade>) +ApplicationGroups/EmailGroups.
File: C:\IDBridge\Plugins\Invoke-PluginInfiniteCampusStudents.ps1. Pulls students via
Get-SourceDataInfiniteCampus (OneRoster 1.2 rostering API; client secret from the vault
secret ApiKey-InfiniteCampus; optional school exclusion by identifier; safety floor).
PersonID/Username = LocalID(the IC student number),InternalID = InternalID(the IC person ID),UPN = <LocalID>@my.yourdistrict.org,PersonTypeID = "1".- Grade from
Grade; building from aSchoolIdentifier→name/code map (fallback000). - Per-grade settings work exactly as in the Skyward plugin (
GradeDefaultSettings+GradeOverridesmerged acrossValidGrades), but password types are limited toRANDOM/API-PASSPHRASE— Infinite Campus OneRoster exposes no food-service PIN or password word. Group-processing scope (AD/Google.ProcessGroups) works the same way too. IDBActive: false if not seen withinDaysLastSeen(14), if IC reports the student as notactive/account-disabled (Status,ActiveUserAccount), if the primary enrollment'sRoleEndDateis in the past, or if the grade is missing/disabled in settings. Grade-12 students who disappear (graduates — IC drops them from the feed rather than flagging them) get the longerDaysLastSeenGraduatewindow (90) before aging out; once past the normal 14-day window they're relabeledGD, moving them to theGrade-GDOU/groups for the rest of the grace (mirrors Skyward's GD handling).- Groups =
Get-CustomStudentGroups -building -grade(bundled:Students, optional<code>_Students,Grade-<grade>); IC has no ApplicationGroups/EmailGroups equivalent.
File: C:\IDBridge\Plugins\Invoke-PluginPostRunReport.ps1. Writes
RunSummary-<timestamp>.json to Paths.ExportsRoot after every run: outcome, timing, mode
flags, actual applied counts, an Applied section (succeeded/failed tallies, one line per
failed write, and a Writes journal — one line per attempted write with PersonID, target,
and outcome: the full "what did the run do" record), per-directory proposed change-list
sizes, threshold results, and the run's Warn/Error log lines (via Get-IDBridgeLogs).
No full user records, but the journal and log lines carry IDs and addresses — fine locally,
think before shipping the file off the box. No placeholders: enable its descriptor and it runs.
File: C:\IDBridge\Plugins\Invoke-PluginPostRunOrphanReport.ps1. Reports enabled
directory accounts that carry a PersonID link (AD EmployeeID / Google externalId type
organization) but matched no source record this run — accounts the pipeline can never
update or deactivate because nothing feeds them (deleted sheet row, student gone before
LastSeen state existed, person removed from the SIS). Compares AD/Google.CurrentUsers
against SourceData.PersonID; accounts without a PersonID link are out of scope (never
IDBridge-managed). Writes OrphanReport-<timestamp>.csv to Paths.ExportsRoot and logs a
Warn with counts when orphans exist. Skips failed runs and TestRuns (capped source would
flag everyone). Report-only — offboard by hand or with a ForceDisable override row.
No placeholders: enable its descriptor and it runs.
File: C:\IDBridge\Plugins\Invoke-PluginPostRunWebhook.ps1. POSTs a compact JSON summary
(success, error text, duration, mode flags, counts — no per-user data) to your own endpoint;
throws until the placeholder URL is edited. Fire-and-forget: 10 s timeout, send failures log
a Warn and never affect the run. The starting point for shipping your own telemetry or
alerting ("tell me when the run breaks").
File: C:\IDBridge\Plugins\Invoke-PluginPostRunExport.ps1. Writes the user list CSVs
defined in its $exportFiles map (file name → the PersonTypeIDs it carries; one file
can combine multiple IDs) to Paths.ExportsRoot, for feeding downstream systems. Defaults
to UserList-Staff.csv ('2','3') and UserList-Students.csv ('1'); edit
$exportFiles to change the files and $exportColumns to change what each row carries.
A file whose IDs match no users logs a Warn and is left untouched. Skipped on failed
runs (a partial dataset must not overwrite the last good export) and on TestRun (same
reason); files are overwritten on every successful full run. No placeholders: enable its
descriptor and it runs. Replaces the UserList-Staff.csv export that older module
versions wrote from Invoke-IDBridge itself.
- Create
C:\IDBridge\Plugins\Invoke-PluginX.ps1definingfunction Invoke-PluginX { param() … }(for PostRun:param([pscustomobject]$RunResult)). - Pull config/secrets via
Get-IDBridgeConfig; log viaWrite-Log. - Return an array of
[PSCustomObject]matching the schema above (Source) or override schema. PostRun plugins return nothing — their value is their side effect (report/POST/export). - Register it in
IDBridgeConfig.psd1PluginswithEnabled,Type, andFunction. - Test with
Invoke-IDBridge -ReadOnly -TraceLoggingand inspect the log + change lists before enabling writes.