For IT professionals and managed service providers (MSPs), maintaining a standardized and efficient user environment is essential. One overlooked but impactful aspect of user experience is the taskbar configuration. Pinning frequently used applications can enhance productivity, streamline workflows, and enforce organizational standards. While this can be achieved manually, automation offers significant advantages in scale and consistency. In this post, we explore a how to pin taskbar items with PowerShell in Windows —a practical tool that brings both precision and efficiency to desktop environment management.
Background
Manually pinning applications to the Windows taskbar may suffice in small environments. However, for larger organizations managing hundreds or thousands of endpoints, manual customization becomes inefficient and error-prone. Microsoft’s lack of a direct command-line utility for pinning applications has led the community to devise workarounds—some clumsy, others effective.
This PowerShell script to pin taskbar items offers a robust solution. It dynamically generates an XML layout that specifies taskbar pins and imports it using Windows’ native Import-StartLayout cmdlet. For organizations using tools like NinjaOne to manage remote systems, this approach becomes especially powerful, allowing seamless taskbar customization during provisioning or routine updates.
The Script
<# .SYNOPSIS Pins items to the taskbar. Required to be run as an Administrator account and not as SYSTEM. .DESCRIPTION Pins items to the taskbar. Required to be run as an Administrator account and not as SYSTEM. This script will use the AppId from Get-StartApps. Running Get-StartApps will list all available applications. You can find the AppId by running Get-StartApps and looking at the "AppId" column. Notes: Clearing all pinned items will not remove pinned items that a user has added themselves. Using environment variables like %APPDATA% will work, but this script might warn you that the file path does not exist if the user running this doesn't have the file in it's %APPDATA% path. If pinning a Windows Store app, use the AppId without the ! and text after the !. For example use "Microsoft.Windows.Photos_8wekyb3d8bbwe" instead of "Microsoft.Windows.Photos_8wekyb3d8bbwe!App". For desktop apps, use the AppID as well. A few examples: Google Chrome's AppId is "Chrome". Internet Explorer's AppId is "Microsoft.InternetExplorer.Default". Microsoft Edge's AppId is "MSEdge". Node.js's AppId is "{6D809377-6AF0-444B-8957-A3773F02200E}\nodejs\node.exe" Notepad++'s AppId is "{6D809377-6AF0-444B-8957-A3773F02200E}\Notepad++\notepad++.exe". My Application in AppData is "%APPDATA%\My Application\MyApplication.exe". By using this script, you indicate your acceptance of the following legal terms as well as our Terms of Use at https://www.ninjaone.com/terms-of-use. Ownership Rights: NinjaOne owns and will continue to own all right, title, and interest in and to the script (including the copyright). NinjaOne is giving you a limited license to use the script in accordance with these legal terms. Use Limitation: You may only use the script for your legitimate personal or internal business purposes, and you may not share the script with another party. Republication Prohibition: Under no circumstances are you permitted to re-publish the script in any script library or website belonging to or under the control of any other software provider. Warranty Disclaimer: The script is provided “as is” and “as available”, without warranty of any kind. NinjaOne makes no promise or guarantee that the script will be free from defects or that it will meet your specific needs or expectations. Assumption of Risk: Your use of the script is at your own risk. You acknowledge that there are certain inherent risks in using the script, and you understand and assume each of those risks. Waiver and Release: You will not hold NinjaOne responsible for any adverse or unintended consequences resulting from your use of the script, and you waive any legal or equitable rights or remedies you may have against NinjaOne relating to your use of the script. EULA: If you are a NinjaOne customer, your use of the script is subject to the End User License Agreement applicable to you (EULA). .EXAMPLE -Application "Photos" Pins the Photos UWP app to the taskbar. .EXAMPLE -Application "Chrome" Pins the Chrome desktop app to the taskbar. .EXAMPLE -Application "Microsoft.Windows.Photos_8wekyb3d8bbwe, Chrome, {1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\WindowsPowerShell\v1.0\powershell.exe" Pins the Photos UWP, Chrome, and PowerShell to the taskbar. .EXAMPLE -Application "Chrome, Microsoft.InternetExplorer.Default, {6D809377-6AF0-444B-8957-A3773F02200E}\nodejs\node.exe" .EXAMPLE -Clear Clears ALL pinned items from the taskbar. PARAMETER: -Application "ReplaceMeWithApplicationName" The name of the application you would like to pin to the taskbar. Running Get-StartApps will list all available applications. PARAMETER: -Clear Clears ALL pinned items from the taskbar. .NOTES Minimum OS Architecture Supported: Windows 10, Windows Server 2016 Release Notes: Initial Release System Layout Path: C:\Users\Default\AppData\Local\Microsoft\Windows\Shell\LayoutModification.xml #> [CmdletBinding()] param ( [String]$Applications, [switch]$Clear, [switch]$Restart ) begin { function Test-IsElevated { $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() $p = New-Object System.Security.Principal.WindowsPrincipal($id) $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) } function Test-IsSystem { $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() return $id.Name -like "NT AUTHORITY*" -or $id.IsSystem } if ($env:applications -and $env:applications -notlike "null") { $Applications = $env:applications } if ($env:clear -and $env:clear -notlike "false") { $Clear = $true } if ($env:restart -and $env:restart -notlike "false") { $Restart = $true } $XmlTemplate = @" <?xml version="1.0" encoding="utf-8"?> <LayoutModificationTemplate xmlns="http://schemas.microsoft.com/Start/2014/LayoutModification" xmlns:defaultlayout="http://schemas.microsoft.com/Start/2014/FullDefaultLayout" xmlns:start="http://schemas.microsoft.com/Start/2014/StartLayout" xmlns:taskbar="http://schemas.microsoft.com/Start/2014/TaskbarLayout" Version="1"> LAYOUTXML <defaultlayout:TaskbarLayout> <taskbar:TaskbarPinList> PINLISTXML </taskbar:TaskbarPinList> </defaultlayout:TaskbarLayout> </CustomTaskbarLayoutCollection> </LayoutModificationTemplate> "@ $DefaultLayout = "<CustomTaskbarLayoutCollection>" $TempLayoutPath = "$env:Temp\layoutmodification$(Get-Random).xml" $HasError = $false } process { if ((Test-IsSystem)) { Write-Host "[Error] This script must be ran as an Administrator and not as SYSTEM." exit 1 } if (-not (Test-IsElevated)) { Write-Host "[Error] This script must be ran as an Administrator." exit 1 } if ($Clear -and $Applications) { Write-Host "[Error] You cannot use both Clear and Applications parameters." exit 1 } $PinnedApps = [System.Collections.Generic.List[String]]::new() # Check if we are clearing pinned apps $PinListContent = if ($Clear) { # Add PinListPlacement="Replace" to CustomTaskbarLayoutCollection $DefaultLayout = "<CustomTaskbarLayoutCollection PinListPlacement=`"Replace`">" # Add <taskbar:DesktopApp DesktopApplicationLinkPath="#leaveempty"/> to TaskbarPinList Write-Output "<taskbar:DesktopApp DesktopApplicationLinkPath=`"#leaveempty`"/>" } elseif ($Applications) { $InstalledAppxPackages = Get-AppxPackage -AllUsers $AumidList = foreach ($app in $InstalledAppxPackages) { # Get the AppId from the manifest file try { $ManifestPath = Join-Path -Path $app.InstallLocation -ChildPath "AppxManifest.xml" $ManifestPathLeaf = Split-Path -Path $app.InstallLocation -Leaf if ($(Test-Path -Path $ManifestPath -ErrorAction SilentlyContinue)) { [xml]$Manifest = Get-Content -Path $ManifestPath -ErrorAction Stop $Name = $Manifest.Package.Identity.Name $Publisher = @( # Get the First and Last part the folder name from the manifest path # From: Microsoft.Windows.Photos_2024.11070.15005.0_x64__8wekyb3d8bbwe # To: Microsoft.Windows.Photos_8wekyb3d8bbwe "$ManifestPathLeaf" -split '_' | Select-Object -First 1 "$ManifestPathLeaf" -split '_' | Select-Object -Last 1 ) -join '_' $Id = $Manifest.Package.Applications.Application.Id if ($Publisher -and $Id -and $Publisher -like "*$Name*") { $Id | ForEach-Object { Write-Output "$($Publisher)!$($_)" } } } } catch {} } Write-Host "[Info] Found $($AumidList.Count) installed Microsoft Store apps." $SoftwareList = Get-StartApps | Select-Object -ExpandProperty AppId Write-Host "[Info] Total installed apps: $($AumidList.Count + $SoftwareList.Count)" # Check if we are pinning apps # Split the applications by comma $Applications -split ',' | ForEach-Object { $Application = $_.Trim() $FoundApp = $SoftwareList | Where-Object { $_ -like "*$Application*" } | Select-Object -First 1 $FoundApp | ForEach-Object { Write-Host "[Info] Found AppId: $($_)" } if ($FoundApp -like "*!*") { # Add UWP App # UWP apps have ! in the AppId Write-Host "[Info] Adding UWP App: $($FoundApp)" Write-Output "<taskbar:UWA AppUserModelID=`"$($FoundApp)`" />" $PinnedApps.Add($Application) } elseif ($FoundApp -notlike "*!*" -and $Application -in $SoftwareList) { # Add Desktop App # Desktop apps don't have ! in the AppId Write-Host "[Info] Adding Desktop App: $($FoundApp)" Write-Output "<taskbar:DesktopApp DesktopApplicationID=`"$($FoundApp)`" />" $PinnedApps.Add($Application) } elseif ($FoundApp -notlike "*!*" -and $Application -notin $SoftwareList) { # Add Pinned Link Path # Save $Application in $Text for testing if the path exists $Text = $Application # Regex to find environment variables, eg %APPDATA%, %CommonProgramFiles(x86)%, etc. $re = [regex]::new('(%[a-zA-Z\(\)0-9_]+%)') if ($re.IsMatch($Text)) { # Replace environment variables with their PowerShell equivalents and resolve the path, eg %SystemRoot% -> $env:SystemRoot -> C:\Windows $re.Matches($Text) | ForEach-Object { $Text = $Text -replace "$_", $(Get-Item -Path "env:$("$_" -replace '%', '')" -ErrorAction SilentlyContinue).Value } } if (-not $(Test-Path -Path $Text -ErrorAction SilentlyContinue)) { Write-Host "[Warn] File path does not exist for ($($Application)) and might fail to launch." } Write-Host "[Info] Adding Pinned Link Path: $($Application)" Write-Output "<taskbar:DesktopApp DesktopApplicationLinkPath=`"$($Application)`" />" $PinnedApps.Add($Application) } else { Write-Host "[Warn] AppId does not exist: $($Application)" } } } # Case-sensitive Replace $XmlTemplate = $XmlTemplate -creplace "LAYOUTXML", $DefaultLayout $XmlTemplate = $XmlTemplate -creplace "PINLISTXML", $PinListContent # Check if the layout contains UWP or Desktop Apps if ($($XmlTemplate -split [System.Environment]::NewLine | Where-Object { $_ -like "*<taskbar:UWA*" -or $_ -like "*<taskbar:DesktopApp*" }).Count -gt 0) { Write-Host "[Info] Successfully created layout." } else { Write-Host "[Error] Missing apps in layout." $HasError = $true } try { # Save XML to temp file Write-Host "[Info] Creating layout file at ($TempLayoutPath)." Set-Content -Path $TempLayoutPath -Value $XmlTemplate -Force -Confirm:$false -ErrorAction Stop Write-Host "[Info] Successfully saved layout file." } catch { Write-Host "[Error] Failed to create layout file." exit 1 } if ($HasError) { exit 1 } try { Import-StartLayout -LayoutPath $TempLayoutPath -MountPath "C:\" -Confirm:$false -ErrorAction Stop if ($Clear) { Write-Host "[Info] Successfully cleared pinned items from the taskbar." } else { Write-Host "[Info] Successfully pinned $($PinnedApps -join ', ') to the taskbar." } } catch { if ($_.Exception.Message -like "*is not a valid layout file*") { Write-Host "" Write-Host $_.Exception.Message Write-Host "" Write-Host "[Error] Failed to pin application to the taskbar." Write-Host "" Write-Host "[Info] Layout Content:" $XmlTemplate | Write-Host } Write-Host "[Error] Failed to pin application to the taskbar." $HasError = $true } # Clean up temp file try { Write-Host "[Info] Removing layout file at ($TempLayoutPath)." Remove-Item $TempLayoutPath -Force -ErrorAction SilentlyContinue Write-Host "[Info] Successfully removed layout file." } catch { Write-Host "[Error] Failed to remove layout file. Template file is located at ($TempLayoutPath)." $HasError = $true } if ($HasError) { exit 1 } if ($Restart) { Write-Host "[Info] Restarting computer." Start-Sleep -Seconds 10 Restart-Computer -Force -Confirm:$false -ErrorAction Stop } exit 0 } end { }
Detailed Breakdown
Let’s dissect how the script works and the mechanisms it employs:
1. Parameter Parsing
The script accepts three main parameters:
- -Application: Comma-separated list of app identifiers or paths.
- -Clear: Removes all pinned items from the taskbar.
- -Restart: Reboots the machine after taskbar changes.
These can also be passed through environment variables, making the script flexible for remote or automated deployments.
2. User Context Validation
Two critical checks ensure the script is run under an appropriate context:
- Must be executed by a user with administrative privileges.
- Cannot be run under the SYSTEM account.
3. Layout XML Generation
The script constructs a temporary XML file conforming to Microsoft’s layout schema. Depending on the input, the XML includes:
- <taskbar:DesktopApp> for traditional Win32 applications.
- <taskbar:UWA> for Universal Windows Platform (UWP) apps.
If the -Clear switch is used, the layout includes a dummy entry with PinListPlacement=”Replace” to purge existing taskbar pins.
4. Application Identification
Applications can be referenced by:
- AppId from Get-StartApps
- AUMID for UWP apps
- Executable paths, including those with environment variables like %APPDATA%
The script intelligently resolves these entries, validates paths, and logs any potential mismatches or errors.
5. Layout Deployment
Once the layout XML is created, it is imported using Import-StartLayout. Temporary files are deleted post-execution, and the system optionally reboots if the -Restart flag is set.
Potential Use Cases
Imagine a mid-sized company onboarding 50 new laptops. Each device needs Chrome, Outlook, and a custom in-house tool pinned to the taskbar for user convenience. Rather than configuring each system manually, an MSP could deploy this script via NinjaOne, passing in the required -Application parameters.
Comparisons
Traditional Methods
- Manual Pinning: Tedious, prone to inconsistency.
- Group Policy Layout XML: Effective but applies to all users and is harder to update dynamically.
- Third-Party Tools: Sometimes powerful but can introduce licensing or trust concerns.
This Script
- Pros: Dynamic, user-level execution, granular control, integrates well with automation platforms like NinjaOne.
- Cons: Requires administrative context, limited to supported Windows versions (Windows 10+, Server 2016+).
FAQs
Q: Can I pin UWP and traditional desktop apps simultaneously?
Yes. The script handles both AppId and AUMID formats.
Q: What happens if the application path doesn’t exist?
A warning is logged, and the entry may not work post-deployment.
Q: Does this affect user-pinned items?
Clearing will not remove user-added pins unless PinListPlacement=”Replace” is used.
Q: Can this be run remotely?
Yes, via RMM tools like NinjaOne or remote PowerShell sessions, provided administrative context is ensured.
Implications
Scripts like these highlight how endpoint automation can significantly impact IT security and usability. Misconfigured taskbars can clutter user experiences and increase support tickets. Worse, they can expose risky apps if left unmonitored. A uniform layout reinforces compliance and usability while reducing endpoint sprawl.
Security-wise, care should be taken to validate paths and ensure only authorized apps are pinned. Automating this process can also assist in hardening machines by pinning only whitelisted software.
Recommendations
- Always run the script as an administrator (not SYSTEM).
- Use Get-StartApps to verify valid AppIds before deployment.
- For best results, test on a non-production machine first.
- Include error-handling routines in automated deployment tools to monitor layout application status.
- Periodically review pinned applications for relevance and security.
Final Thoughts
While the native Windows ecosystem doesn’t make taskbar customization easy, this PowerShell script bridges the gap effectively. It’s especially powerful when combined with an RMM platform like NinjaOne, enabling IT teams to push standardized layouts across fleets of devices with minimal friction. For IT professionals looking to automate UI consistency and improve end-user efficiency, learning how to pin taskbar items with PowerShell is a worthwhile investment.