- PowerShell 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| Changelog.md | ||
| Readme.md | ||
| Update-TeamsVDI-Full.ps1 | ||
Update-TeamsVDI
Installs or updates New Microsoft Teams (and the Meeting Add-in) on non-persistent VDI golden images — Azure Virtual Desktop, Citrix, Omnissa/VMware Horizon, and Windows 365. Detects the OS category and picks the matching installation method, applies VDI registry optimizations, and is safe to re-run on a schedule.
Requirements
- Windows 10/11, Windows Server 2019/2022/2025 (Server 2016 and older: unsupported, the script exits with an error)
- Windows PowerShell 5.1
- Execution context:
NT AUTHORITY\SYSTEM(NinjaOne default) or an elevated interactive session - Internet access to
go.microsoft.com(Teams MSIX, and on Modern OS: the bootstrapper) - On the Legacy route (Server 2019): the AppLocker PowerShell module (
Get-AppLockerFileInformation), used to read the Meeting Add-in's version — present by default on Server 2012+ but confirm it hasn't been removed via Features on Demand.
NinjaOne variables
| Variable | Type | Default / Example | Purpose |
|---|---|---|---|
$env:simulateRun |
Checkbox | true |
Dry-run: log every intended action without changing anything. Set to false for a real install/update. |
$env:installTMA |
Checkbox | true |
Install the Teams Meeting Add-in (Outlook plugin) |
$env:disableTeamsAutoUpdate |
Checkbox | true |
Set disableAutoUpdate = 1 so Teams doesn't update itself outside of image builds |
$env:removeExisting |
Checkbox | true |
Remove the existing Teams installation/provisioning before reinstalling |
$env:cleanupStagingAfterRun |
Checkbox | true |
Delete downloaded files (MSIX, bootstrapper, TMA installer) from the staging folder after the run |
$env:failOnPrerequisiteWarning |
Checkbox | false |
Abort instead of warn-and-continue when WebView2 or FSLogix are below the minimum version |
$env:minWebView2Version |
String | 90.0.818.66 |
Minimum required WebView2 Runtime version |
$env:minFSLogixVersion |
String | 2.9.8884.27471 |
Minimum required FSLogix version (only checked when FSLogix is actually present) |
Optional: a teamsVersionInstalled custom field (Text, script read/write permission) is written with the installed Teams version at the end of each run, if it exists. The script skips this silently if the field isn't defined or the write fails — it is never fatal to the run.
Usage
Simulated run (do this first)
Set simulateRun to true (the default) and run against one pilot device. Review C:\Windows\Temp\Update-TeamsVDI.log and confirm every [SIMULATION] line targets what you expect.
Manually:
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\Update-TeamsVDI.ps1 -SimulateRun
Live run
Set simulateRun to false in the NinjaOne policy, roll out to a pilot group, then to the full scope.
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\Update-TeamsVDI.ps1
Logging
- Location:
C:\Windows\Temp\Update-TeamsVDI.log - Format:
[YYYY-MM-DD HH:mm:ss] [LEVEL] message - Levels:
OK,INFO,SIMULATION,WARN,ERROR - Rotates at 5 MB; archived logs (
Update-TeamsVDI_<timestamp>.log) are removed after 30 days. - Staging folder for downloaded files:
C:\Windows\Temp\Update-TeamsVDI\(not the interactive user's Downloads folder — that path is not usable underSYSTEM).
Exit codes
| Code | Meaning |
|---|---|
| 0 | Success (including "already up to date" and "nothing needed to be done") |
| 1 | Failure — see the [ERROR] lines in the log |
| 2 | Success, but a reboot is recommended (an installer returned exit code 3010) |
NinjaOne note: by default, NinjaOne treats any non-zero exit code as a failed result, which means exit code 2 will show the same as exit code 1 unless the policy/condition is explicitly configured to treat 2 as a non-failure. This is a deliberate choice in this script — reboot-pending is surfaced loudly via a distinct exit code rather than silently folded into
0— but it does mean the NinjaOne side needs to be set up to recognize it, or exit 2 will read as "failed" on the dashboard.
Behaviour
-
Idempotent: compares the installed Teams version against the latest available MSIX before doing anything. A match skips the update entirely — but VDI registry keys are still applied and verified on every run.
-
OS-aware routing, decided once at the very start:
OS Category Installation method TMA installation Windows 10 / 11 Modern teamsbootstrapper.exe -p -oAutomatic via --installTMAServer 2022 (build ≥ 20348.2402) Modern teamsbootstrapper.exe -p -oAutomatic via --installTMAServer 2025 (build ≥ 26100.2886) Modern teamsbootstrapper.exe -p -oAutomatic via --installTMAServer 2019 Legacy Dism /Online /Add-ProvisionedAppxPackage /SkipLicenseAutomatic — MSI extracted from the MSIX, installed via msiexecServer 2016 or older Unsupported — — -
TMA re-installs are idempotent: on the Legacy route, an existing Meeting Add-in installation is looked up via the Windows Installer Uninstall registry (not just its folder). A matching version is left alone; a different version is removed with
msiexec /xfirst. This avoids exit code 1638 ("another version of this product is already installed") on repeated runs. -
Simulation mode is on by default. Every state-changing action — registry writes, package removal, provisioning, TMA install/uninstall, stopping running Teams processes — is gated behind it and logs exactly what it would do, including the concrete target. Read-only steps (version checks, downloads needed to determine the target version) always run, since they don't change anything on the machine.
-
Not silent by default in the sense of "does nothing": because
simulateRundefaults totrue, a policy deployed without explicitly setting it tofalsewill detect and log everything but change nothing. This is intentional — confirm the simulation output first. -
What is not handled: this script does not install or update WebView2 or FSLogix themselves, only checks their version. It also does not manage Citrix VDA, AVD agent, or Omnissa agent versions. Reboot recommendations (exit code 2) are surfaced but the script does not initiate a reboot itself.
-
Maintainability: every
$env:variable is resolved exactly once, at the top of the entry point (bottom of the file, right beforeInvoke-Mainis called), and passed in as an explicit named parameter from there. No function reads$env:directly. The full variable table is also duplicated in the script's own.NOTESheader comment, so it's visible immediately on opening the file — not just in this README.