Over the last 15 years I’ve tried pretty much every method of adding printers at logon there is – KIXTART script, VBS, Group Policy Preferences and Powershell. As part of speeding up logon, and investigating a weird issue with Windows 10 printers, I moved away from GPP and to Powershell shortly after we upgraded from Windows 8.1 to Windows 10.

The issue being – roughly 5% of the time, on random user/computer combinations, printers would take a long time adding and then fail to add, with a non-specific error message. My first go at this was a basic powershell script which had a hard coded list of location/printer mapping, and it would run the “add printer” command repeatedly until the error went away. (It always added fine on the 2nd go). The problem with this is that it’s a complicated script for technicians to update, and being a single threaded script the nice form it displays showing people what’s happening would freeze while it was working in the background.
My new script does the bulk of the work in background jobs – so printers add quicker (as it can do more than one at once), and the UI doesn’t lock up and freeze. More importantly, it uses Group Policy Preferences by reading the XML file generated and applies that – so technicians have the familiar interface for adding/removing printers from the script.
First step for this is to create a group policy object – call it anything you like, mine is “Printers v3”. Make sure that you DO NOT link it to any OU (or if you do, the link is disabled) as we do not want this processed by the group policy client, this is purely to generate the XML file for the powershell script.
I’ll leave a detailed howto on group policy preferences but you should be able to find what you need with a quick search on the Internet.
The script uses the User Configuration set of preferences – as you can’t add shared printers to the Computer section, and currently only supports shared (i.e. networked and shared off a Windows server) printers.

Set up your list of printers – it doesn’t matter whether you select Create, Replace or Update as they all do the same thing in this instance. If you set multiple printers to be default, whichever runs last will win.

Now on to the script. You will need the GUID of the policy, this can be found in Group Policy Management Console by selecting the GPO, then going onto the “Details” tab, copy the Unique ID and insert that where prompted near the start of the script, along with your fully qualified domain name (on the same screen, this is “Domain” at the top).
As the preferences file is XML it’s easy to read into powershell.
When displaying the printer details to the end user, it will show whatever is down as the printer name in the GPP section. By default this is the share name, however you can rename this by selecting the item and hitting F2.
Set this as the logon script in Group Policy – you will need to make sure you are allowing all scripts to run (Computer Configuration->Policies->Windows Components->Windows Powershell->Turn on Script Execution) or set it as a non-Powershell logon script with the command:
powershell.exe -executionpolicy bypass -file \\path\to\printers.ps1"
# Printers Logon Script # # Uses a Group Policy Object's User Preference items to add printers. # Create the GPO but do not link it, it should be used by this script but NOT processed by group policy at logon. # # Katy Nicholson 04/08/2020 # katynicholson.uk # Set your domain and the GUID of the policy object here. $domain = "fully.qualified.domain.name" $policyGUID = "{C96580A0-00DC-4E63-899A-3D82CF5EE141}" $GPOxmlFile = "\\$domain\sysvol\$domain\Policies\$policyGUID\User\Preferences\Printers\Printers.xml" #Set up the form to show progress to the user Add-Type -AssemblyName System.Windows.Forms #Force the form to show $t = '[DllImport("user32.dll")] public static extern bool ShowWindow(int handle, int state);' try{ Add-Type -Name win -Member $t -Namespace native [native.win]::ShowWindow(([System.Diagnostics.Process]::GetCurrentProcess() | Get-Process).MainWindowHandle, 0) } catch { $Null } $global:PrinterForm = New-Object System.Windows.Forms.Form $global:Font = New-Object System.Drawing.Font("Calibri", 12) $global:LabelTop = 25 $PrinterForm.ClientSize = '500,300' $PrinterForm.text = "Printers" $PrinterForm.BackColor = "#ffffff" $Label1 = New-Object System.Windows.Forms.Label $Label1.Text = "Please wait while your printer connections are configured..." $Label1.Font = $Font $Label1.Width = 500 $Label1.Height = 30 $Label1.Top = 10 $Label1.Left = 10 $PrinterForm.Controls.Add($Label1) [void]$PrinterForm.Refresh() [void]$PrinterForm.Show() [void]$PrinterForm.Focus() #Supporting functions #Removes domain when in the format DOMAIN\username Function stripDomain($item) { if ($item.IndexOf('\') -gt 0) { return $item.Substring(($item.IndexOf('\')+1)) } else { return $item } } #Searches AD to check if the specified username is in the specified group Function isInGroup([String]$groupname, [String]$username) { #Strip domain from group and user names, if present $groupname = stripDomain($groupname) $username = stripDomain($username) if ($group = ([ADSISearcher]"sAMAccountName=$groupname").FindOne()) { $groupDN = $group.Properties['distinguishedName'] if ($isMember = ([ADSISearcher]"(&(|(objectClass=computer)(objectClass=person))(sAMAccountName=$username)(memberOf:1.2.840.113556.1.4.1941:=$groupDN))").FindOne()) { return $true } } return $false } #Evaluate the item targeting filters Function evaluateFilters($Filters) { $FilterResults = @() foreach ($Filter in $Filters.ChildNodes) { #Process filters on this item [Bool]$return = $false switch ($Filter.LocalName) { "FilterCollection" { #Recurse through collections return evaluateFilters $Filter } "FilterGroup" { #Filter to whether the user (or computer) is a member of the specified group if ($Filter.userContext -eq "1") { $username = $env:USERNAME } else { $username = "$env:COMPUTERNAME$" } if (isInGroup $Filter.name $username) { $return = $true } break } "FilterComputer" { #Filter if the computer is the specified name if ($env:COMPUTERNAME -ieq (stripDomain $Filter.name)) { $return = $true } break } "FilterUser" { #Filter if the user is the specified name if ($env:USERNAME -ieq (stripDomain $Filter.name)) { $return = $true } break } "FilterOrgUnit" { #Filter if the user or computer is a member of the specified OU (either directly or in a child OU) if ($Filter.userContext -eq "1") { $itemDN = ([ADSISearcher]"sAMAccountName=$env:USERNAME").FindOne().Properties.distinguishedname[0] } else { $itemDN = ([ADSISearcher]"sAMAccountName=$env:COMPUTERNAME$").FindOne().Properties.distinguishedname[0] } if ($itemDN.IndexOf($Filter.name, [System.StringComparison]::OrdinalIgnoreCase) -gt -1) { #Item is in the OU $OUPath = $itemDN.SubString($itemDN.IndexOf("OU=")) if ($Filter.directmember -eq "1") { $return = ($OUPath -ieq $Filter.name) } else { $return = $true } } break } } # If filter is set to NOT then negate return value if ($Filter.not -eq "1") { $FilterResults += ,(@($Filter.bool.ToString(),(!$return))) } else { $FilterResults += ,(@($Filter.bool.ToString(),($return))) } } #Evaluate the filters = AND or OR the first two results, depending on what is configured, then AND/OR this with the next value, repeat until done. $previousItem = $true foreach ($item in $FilterResults) { if ($item[0] -eq "AND") { if ($previousItem -and $item[1]) { $previousItem = $true } else { $previousItem = $false } } else { if ($previousItem -or $item[1]) { $previousItem = $true } else { $previousItem = $false } } } return $previousItem } #Update the status labels on the form Function updateStatus($printerName, $status) { foreach ($Label in $PrinterForm.Controls) { if ($Label.Tag -and $printerName -eq $Label.Tag.Printer.ToString()) { $Label.Text = $Label.Tag.Display.ToString() + " - " + $status return } } $PrinterForm.Refresh() } #Add an item status label to the form Function createStatus($printerName, $displayName, $status) { $LabelTop += 25 $NewLabel = New-Object System.Windows.Forms.Label $NewLabel.Text = $displayName + " - " + $status $NewLabel.Tag = [PSCustomObject]@{ "Printer"=$printerName "Display"=$displayName } $NewLabel.Font = $Font $NewLabel.Width = 500 $NewLabel.Height = 20 $NewLabel.Top = $LabelTop $NewLabel.Left = 20 $PrinterForm.Controls.Add($NewLabel) $PrinterForm.Refresh() Set-Variable -Name "LabelTop" -Value $LabelTop -Scope Global } [xml]$Printers = Get-Content -Path $GPOxmlFile #Logic for adding printers, to run in background job $AddPrinterScriptBlock = { Function AddPrinter([String]$printer, $default) { # When Windows 10 decides it doesn't want to add on the first go at random intervals - retry forever until it adds try { Add-Printer -ConnectionName $printer -ErrorAction Stop } catch { AddPrinter $printer $default } #Set default printer if specified if ($default -eq "1") { (New-Object -ComObject WScript.Network).SetDefaultPrinter($printer) } } AddPrinter $args[0] $args[1] } #Logic for removing printers, to run in background job $RemovePrinterScriptBlock = { Function RemovePrinter([String]$printer) { if ($printer -eq "All") { #Remove all printer connections Get-Printer | Where {$_.Type -eq "Connection"} | Remove-Printer } else { #Remove specified printer connections (New-Object -ComObject WScript.Network).RemovePrinterConnection($printer) } } RemovePrinter $args[0] } #Loop through each printer connection in the GPP list, if it has item level targeting (filters) then evaluate these. If matched, perform the relevant action. foreach ($Printer in $Printers.Printers.SharedPrinter) { [Bool]$matched = $true if ($Printer.Filters.HasChildNodes) { $matched = evaluateFilters $Printer.Filters } if ($matched -eq $true) { if ($Printer.Properties.action -eq "D") { if ($Printer.Properties.deleteAll -eq "1") { #Delete All network printers - we Wait-Job on this one as we don't want it running at the same time as we are creating printer connections. #Delete All needs to be priority 1 in the GPP list. createStatus "DeleteAll" "Clean up old printers" "Running..." Start-Job $RemovePrinterScriptBlock -Name "DeleteAll" -ArgumentList "All" | Out-Null Wait-Job -Name "DeleteAll" updateStatus "DeleteAll" "Complete" } else { createStatus $Printer.name ("Remove " + $printer.name.ToString()) "Not started" Start-Job $RemovePrinterScriptBlock -Name $printer.name.ToString() -ArgumentList $Printer.Properties.path | Out-Null } } else { createStatus $Printer.name ("Add " + $Printer.name.ToString()) "Not started" Start-Job $AddPrinterScriptBlock -Name $Printer.name.ToString() -ArgumentList @($Printer.Properties.path, $Printer.Properties.default) | Out-Null } } } #Check on the background jobs, updating the UI. While ($jobs = Get-Job -State "Running") { ForEach ($job in $jobs) { switch ($job.State) { "Running" { updateStatus $job.Name "Running..." break } "Completed" { updateStatus $job.Name "Complete" break } "Failed" { updateStatus $job.Name "Failed" break } } } Start-Sleep -Milliseconds 100 [System.Windows.Forms.Application]::DoEvents() } Start-Sleep 1 $PrinterForm.Hide() #Clean up Get-Job | Remove-Job