Stop the print spooler, delete stuck print jobs and start the spooler again on a Windows device
Clear print jobs that are stuck on a Windows workstation, so new jobs print again, and report how many were removed. For MSPs and IT teams who get "nothing is printing" tickets.
| Runs in | On the device (PowerShell, as SYSTEM) |
| Uses | None |
| Suggested approval | None, so only Engineers and Admins can run it. To let end users ask for it, add Anyone → Require approval from an engineer group |
| Category | Devices |
Test it with SuperIT before you rely on it
This is an example, not a ready-made action, and it hasn't been tested end to end. Before you use it, work through it with SuperIT: ask SuperIT to review the script against your environment and requirements, and adjust it together. Then run it on a test device, as in Test it. Device actions can't use Test, so every run is real.
Why use an action instead of asking SuperIT
Clearing a stuck queue means stopping the Print Spooler and deleting files in a system folder. Stop-Service is denied by the default command policies, and the spool folder needs admin rights, so SuperIT can't do it in a chat, and end users are never asked to approve commands. This action does exactly this one job as SYSTEM and isn't checked against command policies; its authorization policy decides who can run it. Because it removes every queued job on the device, it refuses to run on servers and on devices that share printers, where it would clear other people's jobs too.
To see which printers a device has and how many jobs are queued, just ask SuperIT in chat.
When to use it
- "Nothing is printing and the queue says 'Error - Printing'."
- "I can't delete the job stuck at the top of the queue."
- "The printer works for everyone except me."
- A ticket investigation on a printing ticket finds jobs stuck on RECEPTION-PC. An engineer then runs the action on RECEPTION-PC from a chat.
What the action does
- Stops without changing anything if
expected_hostnameisn't this device's computer name. - Refuses to run on Windows Server, or on a device that shares any of its printers, such as a print server.
- Lists the device's printers and how many jobs each has.
- Records the services that depend on the Print Spooler, directly or through other services, and are running.
- Stops the Print Spooler.
- Deletes the job files in
C:\Windows\System32\spool\PRINTERS. - Starts the Print Spooler and its dependent services again.
- Prints how many job files were removed and the printers' job counts afterwards.
What it doesn't do:
- It doesn't keep any jobs. Every queued job on the device is removed, for every printer and every user.
- It doesn't run on print servers or other servers. Clear a print server's queue by hand.
- It doesn't reinstall drivers or remap printers.
- It may not list printers the user connected to themselves. Shared network printers added by a user belong to that user's session, which an action running as SYSTEM can't see. Their stuck jobs are still cleared, because they're held in the same spool folder.
Before you begin
- A Windows device with the SuperIT RMM agent, online. Actions run on Windows devices only; the macOS and Linux agents are in Beta and can't run actions. See Requirements.
- Your role. Creating an action in a chat needs the Admin role. Engineers and Admins can build it under Workflows → Actions.
- No credential. See Credentials.
- An authorization policy, only if end users should be able to ask for it. With None, only Engineers and Admins can run it. To let end users ask, link a policy with a rule Requester Anyone, Approver Require approval from an engineer group, such as
superit:engineers. See Authorization policies and approvals.
Device actions and end users
A device action runs as SYSTEM on the device chosen for the run, or on the action's default device or your team's designated device if none is chosen. SuperIT doesn't check that the device belongs to the person who asked. Never give this action an Anyone → No approval needed rule. Approvers should check which device the run is for before approving.
Build it with SuperIT
Create an action called "Clear a stuck print queue".
Runtime: PowerShell, running on the device as SYSTEM (execution target: device).
Input: expected_hostname (string, required).
Stop before changing anything unless expected_hostname matches $env:COMPUTERNAME. Refuse if Win32_OperatingSystem ProductType isn't 1 (workstation) or any printer is shared (read the Attributes of each printer under HKLM:\SYSTEM\CurrentControlSet\Control\Print\Printers, so a broken spooler can't hide a share).
List printers with job counts. Note running services that depend on Spooler, directly or indirectly. Stop Spooler, wait for Stopped, delete files in C:\Windows\System32\spool\PRINTERS, start Spooler and the dependent services, wait for Running.
Print the number of job files removed and the printers' job counts after. Windows PowerShell 5.1 compatible.
No authorization policy.
SuperIT confirms the brief, drafts the script, validates it and creates the action as a draft. Review the script, then publish it under Workflows → Actions.
Or build it yourself
Create the action under Workflows → Actions with the PowerShell runtime and this input, then set it to run on a device.
| Name | Type | Required | Example |
|---|---|---|---|
expected_hostname |
string | Yes | RECEPTION-PC |
On a device, the script runs in Windows PowerShell as SYSTEM with $ErrorActionPreference = "Stop" already set. Inputs arrive as strings in $ctx.inputs. Invoke-PrintQueueClear at the top lists the steps in order; each step is a small function below it, and the last line runs it. $result is set as $script:result, because a plain $result = inside a function stays local and never reaches the Result tab.
# On one workstation, stops the Print Spooler, deletes every queued job file, starts the spooler and
# its dependants again, and prints the printers' job counts before and after.
function Invoke-PrintQueueClear {
param($ExpectedHostname)
$spoolDir = Join-Path $env:SystemRoot 'System32\spool\PRINTERS'
Assert-TargetDevice -ExpectedHostname $ExpectedHostname
Assert-Workstation
Assert-SpoolerEnabled
Write-Output '=== PRINT QUEUE CLEAR ==='
Write-PrinterQueue -Label 'Before:'
$dependents = @(Get-RunningDependent -ServiceName 'Spooler')
$removed = 0
try {
Stop-Spooler
$removed = Remove-SpoolFile -Path $spoolDir
} finally {
# Stopping can fail part-way after dependants have stopped, and a delete can fail too;
# either way, start the spooler and the dependants that were running, or nobody can print.
Start-Spooler -Dependents $dependents
}
Write-Output "Removed $removed spool file(s) from $spoolDir."
Write-Output "Print Spooler is running."
Write-PrinterQueue -Label 'After:'
# $result is read after the script ends, so set it at script scope, not in this function.
$script:result = @{ files_removed = $removed; spooler = 'Running'; restarted_dependants = $dependents }
}
function Assert-TargetDevice {
param($ExpectedHostname)
$expected = ("$ExpectedHostname".Trim() -split '\.')[0]
if (-not $expected) { throw "expected_hostname is required: enter this device's computer name." }
if ($expected -ne $env:COMPUTERNAME) { throw "This run reached '$env:COMPUTERNAME', not '$expected'. Nothing was changed." }
}
function Assert-Workstation {
# On a server or a device sharing printers, the queue holds other people's jobs too.
if ((Get-CimInstance Win32_OperatingSystem).ProductType -ne 1) {
throw "$env:COMPUTERNAME is a server. Clear its print queue by hand."
}
$shared = @(Get-SharedPrinter)
if ($shared.Count -gt 0) {
throw "$env:COMPUTERNAME shares printers ($($shared -join ', ')). Clear its print queue by hand."
}
}
function Get-SharedPrinter {
# Read the registry, not Get-Printer: a stuck spooler makes Get-Printer fail, and that mustn't pass as "nothing shared".
foreach ($key in @(Get-ChildItem -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Print\Printers' -ErrorAction Stop)) {
$attributes = (Get-ItemProperty -Path $key.PSPath -ErrorAction Stop).Attributes
# 0x8 is PRINTER_ATTRIBUTE_SHARED. A printer whose attributes can't be read counts as shared.
if ($null -eq $attributes -or ($attributes -band 0x8)) { $key.PSChildName }
}
}
function Assert-SpoolerEnabled {
if ((Get-CimInstance Win32_Service -Filter "Name='Spooler'").StartMode -eq 'Disabled') {
throw "The Print Spooler service is disabled on $env:COMPUTERNAME."
}
}
function Write-PrinterQueue {
param([string]$Label)
Write-Output $Label
foreach ($p in @(Get-Printer -ErrorAction SilentlyContinue)) {
$jobs = @(Get-PrintJob -PrinterName $p.Name -ErrorAction SilentlyContinue)
Write-Output (" {0}: {1} job(s), status {2}" -f $p.Name, $jobs.Count, $p.PrinterStatus)
}
}
function Get-RunningDependent {
param([string]$ServiceName, [hashtable]$Seen = @{})
# Walk the whole chain: Stop-Service -Force also stops services that depend on the dependants.
foreach ($dep in (Get-Service -Name $ServiceName).DependentServices) {
if ($Seen.ContainsKey($dep.Name)) { continue }
$Seen[$dep.Name] = $true
if ($dep.Status -eq 'Running') { $dep.Name }
Get-RunningDependent -ServiceName $dep.Name -Seen $Seen
}
}
function Stop-Spooler {
Stop-Service -Name Spooler -Force
(Get-Service -Name Spooler).WaitForStatus('Stopped', [TimeSpan]::FromSeconds(30))
}
function Remove-SpoolFile {
param([string]$Path)
$files = @(Get-ChildItem -Path $Path -File -Force -ErrorAction SilentlyContinue)
$files | Remove-Item -Force
$files.Count
}
function Start-Spooler {
param([string[]]$Dependents)
Start-Service -Name Spooler
(Get-Service -Name Spooler).WaitForStatus('Running', [TimeSpan]::FromSeconds(30))
foreach ($dep in $Dependents) { Start-Service -Name $dep -ErrorAction SilentlyContinue }
}
Invoke-PrintQueueClear -ExpectedHostname $ctx.inputs.expected_hostname
Stopping the spooler and deleting the files both happen inside try, and the finally block starts the spooler and the services that depended on it again. So a stop that fails part-way, or a file that can't be deleted, doesn't leave the device unable to print. Each print job uses one or two files (.SHD and .SPL), so the number of files is not the number of jobs.
Test it
You can't use Test on an action that runs on a device. (On a cloud-sandbox action, Test isn't a dry run either: it runs the script for real and skips approvals.) Every device run is real and removes every queued job, so use a test device.
- On the test device, pause a printer and print a test page so a job is queued.
- Publish the action under Workflows → Actions.
- Select Run, enter the test device's computer name in
expected_hostname, pick that device under Run target, and select Run action. - Check Stdout: the printer shows one job before and none after.
- Run it with a different name in
expected_hostnameand confirm it fails with "Nothing was changed".
Use it
- In a chat. Select the device and ask "The print queue on this device is stuck, can you clear it?". SuperIT asks whether to run on the device and fills in
expected_hostname. Check it's the device you selected. - From the app. Open the action, select Run, enter the device's name in
expected_hostname, pick the same device under Run target, and select Run action. - By end users, with approval. With an Anyone → Require approval from rule, an end user can ask SuperIT in self-service chat and the run waits for an engineer to approve it. End users see whether the run succeeded, not its output, so they find out by printing again.
- Not from tickets automatically. Investigations can recommend it on printing tickets, but don't switch on Run automatically for this action. A run started from a ticket's Recommended actions panel goes to the action's default device or your team's designated device, not the computer of the person who raised the ticket. To act on a ticket, open a chat, select the device and run it there.
- For clients. MSPs can publish it to managed teams.
Always pick the device. If no device is chosen, SuperIT runs a device action on the action's default device or your team's designated device, and expected_hostname stops the run if that's the wrong one.
Variations
- Clear one printer only: add a
printer_nameinput and useGet-PrintJob -PrinterName $name | Remove-PrintJobinstead of stopping the spooler. Stuck jobs sometimes can't be removed this way. - Also print a test page afterwards with
Invoke-CimMethodonWin32_Printer(PrintTestPage). - Add a shared printer for every user of the device with
rundll32 printui.dll,PrintUIEntry /ga /n\\server\printer.Add-Printer -ConnectionNamedoesn't work for this, because as SYSTEM it would add the printer only for the SYSTEM account.