Windows services are the one part of a server that keeps running after everyone logs off, and PowerShell reads and writes them with two cmdlets. Get-Service returns an object for every service; Set-Service changes that service’s start type, status, display name, description, logon account or security descriptor. Both are Windows only, and both are older than most of the PowerShell you write around them.
They are the read half and the write half of the same object, which is exactly why they trip people up. The object carries less than the Services console shows you, and the write side accepts more than it should: one documented value that tab completion offers for -StartupType changes nothing at all and reports success.
This page is about the five places that behaviour is not what the syntax suggests. The enum values and error message templates quoted here are taken from the cmdlet’s own published source at tag v7.4.6, the version boundaries from the 5.1 and 7.x reference pages side by side, and the parameter binding results were measured rather than assumed.
Applies to: Windows PowerShell 5.1 and PowerShell 7.x on Windows 10, Windows 11, Windows Server 2016 through 2025
Quick answer
Read the service before you touch it, change it, then read it back. Set-Service prints nothing unless you ask for output, so -PassThru is what turns it from a silent write into a confirmed one.
# capture what you are about to change - this is your undo
$baseline = Get-Service -Name Spooler | Select-Object Name, Status, StartType, StartupType
$baseline
# StartupType and Status are independent: this sets both in one call
Set-Service -Name Spooler -StartupType Manual -Status Running -PassThru
The first command prints the state you are about to change, which on a default install looks like this:
Name Status StartType StartupType
---- ------ --------- -----------
Spooler Running Automatic Automatic
The second prints the changed service in the default three column view, and its Status column is what confirms the start. Keep $baseline in the session: the last example below restores from it.
-StartupType sets how the service starts at boot. It does not start or stop anything by itself. -Status is the parameter that does that, and the two are independent.
What Get-Service and Set-Service do
Get-Service calls the Service Control Manager and returns a System.ServiceProcess.ServiceController object for each match. On PowerShell 6.0 and later it also opens each service with SERVICE_QUERY_CONFIG and attaches five extra properties that the raw .NET object does not have.
Set-Service writes back through the same Service Control Manager. Its reference page states the requirement plainly: “Set-Service requires elevated permissions. Use the Run as administrator option.” A non-elevated attempt returns a non-terminating error, not a prompt.
The parameters that actually matter, and the part of each that surprises people:
| Parameter | Cmdlet | What it does | Watch for |
|---|---|---|---|
-Name | both | Selects by service name, not display name | Wildcards are permitted on Get-Service and are not permitted on Set-Service |
-DisplayName | both | Selects on Get-Service, renames on Set-Service | Same spelling, opposite meaning |
-StartupType | Set-Service | Sets the boot start mode | An enum, and the member list changed between 5.1 and 7.x |
-Status | Set-Service | Starts, stops or pauses the service | A string with a fixed list: Paused, Running, Stopped |
-Force | Set-Service | Stops dependent services first | “This parameter only works when -Status Stopped is used” |
-PassThru | Set-Service | Returns the changed service | Without it Set-Service writes no output at all |
-RequiredServices | Get-Service | Returns what this service depends on | Changes what the returned object carries, see below |
-DependentServices | Get-Service | Returns what depends on this service | Same caveat |
Before the first example
Four things have to be true before any of the examples below behave as written. Each one is a single command.
One. You are on Windows. Both reference pages open with the same bold line, “This cmdlet is only available on the Windows platform.” On a Linux or macOS build of PowerShell 7 the command does not exist at all, so the failure is a command-not-found error rather than an empty result.
# on Windows this returns the cmdlet; on Linux or macOS it returns nothing
Get-Command Get-Service -ErrorAction SilentlyContinue
Two. You know which PowerShell you are in. This decides which values -StartupType will accept, and it is the single most useful thing to check before running someone else’s service script.
# 5.1 means Windows PowerShell, 7.x means PowerShell 7
$PSVersionTable.PSVersion
Three. The session is elevated. Set-Service needs it, Get-Service does not. Checking is one line and it is the same check the runas and UAC token filtering article uses.
# True only in an elevated session
([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole(
[Security.Principal.WindowsBuiltInRole]::Administrator)
Four. You have captured the current settings of the service you are about to change. Every example below uses the Print Spooler, which exists on every Windows install and is safe to stop and start on a machine that is not printing.
# the same variable the quick answer used; capture it before you change anything
$baseline = Get-Service -Name Spooler |
Select-Object Name, Status, StartType, StartupType
$baseline
StartType and StartupType are two different properties on PowerShell 7 and they are both real. StartType is the .NET one and never says delayed. StartupType is the one PowerShell adds, and it does. The section on the object below pulls them apart.
Practical examples
1. Read the whole service, not just its status
The problem: the default table shows Status, Name and DisplayName, so people reach for the Services console or sc.exe the moment they need the binary path or the logon account.
The solution: on PowerShell 6.0 and later those fields are already on the object, they are just not in the default view. Ask for them by name.
# BinaryPathName and UserName exist from PowerShell 6.0 onward - on 5.1 they are blank
Get-Service -Name Spooler |
Select-Object Name, Status, StartType, StartupType, DelayedAutoStart, UserName, BinaryPathName |
Format-List
If the last four fields come back empty, you are on Windows PowerShell 5.1 rather than PowerShell 7. That is a version difference, not a permissions problem, and the section on the object covers the two other ways they come back empty.
2. Find every automatic service that is not running
The problem: after a patch reboot, something that should have started did not, and nobody noticed until a user did.
The solution: one filter over the whole service list. This is the query that justifies the cmdlet existing.
# StartType is the .NET property, so Automatic here covers delayed-auto services too
Get-Service |
Where-Object { $_.StartType -eq 'Automatic' -and $_.Status -ne 'Running' } |
Select-Object Name, DisplayName, StartType, Status
Automatic in StartType, because the .NET enum has no separate member for it. To split the two you need the DelayedAutoStart property that PowerShell adds, or the DelayedAutoStart field from CIM.
3. Change a startup type and read it back
The problem: a third party agent keeps starting at boot on a server where it is not licensed, and you want it set to manual on a dozen machines without opening a console.
The solution: Set-Service with -StartupType, then read the value back from the service rather than trusting the absence of an error.
# -PassThru is what makes the write visible; without it this prints nothing
Set-Service -Name Spooler -StartupType Manual -PassThru |
Select-Object Name, StartType
Name StartType
---- ---------
Spooler Manual
Those two columns are the shape the reference page’s own Example 10 prints for the same pipeline. What they do not show is the running state, and that is the point: the service is still running. Changing the start type does not stop anything that has already started, so the configured state and the running state have now diverged with nothing in the output to say so.
4. Stop a service that has dependents
The problem: Stop-Service refuses to stop and the error mentions dependent services, so the obvious next move is to stop them by hand one at a time.
The solution: look at the dependency list first, then let -Force stop the chain in the right order. The message you get without it is a fixed template in the product’s own resource file, with only the display name and the service name filled in:
Cannot stop service '{1} ({0})' because it has dependent services. It can only be stopped if the Force flag is set.
# what would be stopped alongside it; an empty result means -Force is not needed
Get-Service -Name Spooler -DependentServices | Select-Object Name, Status
# -Force stops the dependents first, then the target
Stop-Service -Name Spooler -Force
Stopped is not a reason to leave -Force off.
5. Put the service back exactly as it was
The problem: the diagnosis is finished and the machine is now in whatever state the last command left it, which is how a test becomes an outage three weeks later.
The solution: restore from the baseline captured before the first example, then read the service again and compare. A silent comparison that prints nothing is the confirmation.
# StartupType knows about delayed start and StartType does not, so prefer it
$restore = if ($baseline.StartupType) { $baseline.StartupType } else { $baseline.StartType }
Set-Service -Name Spooler -StartupType $restore
if ($baseline.Status -eq 'Running') { Start-Service -Name Spooler }
# now prove it: this should print nothing at all
$after = Get-Service -Name Spooler | Select-Object Name, Status, StartType
Compare-Object $baseline $after -Property Status, StartType
Compare-Object writing no output means the two objects match on both properties. An empty result is the pass condition here, so do not read it as the command having failed.
Restoring from StartupType rather than from StartType matters on a delayed auto-start service, because StartType reports it as plain Automatic and a restore built on that quietly drops the delay. The fallback covers Windows PowerShell 5.1, where StartupType does not exist at all.
The startup type that changes nothing
-StartupType accepts five values on PowerShell 7. Four of them do what their name says. The fifth is documented like this, in the parameter’s own list on the reference page:
InvalidValue - Has no effect. The cmdlet does not return an error but the StartupType of the service is not changed.
It is a full member of the enum, so it passes parameter validation, it appears in tab completion, and it is accepted from a variable or a CSV column without complaint. The cmdlet source shows why it does nothing: the enum’s underlying numbers are the Win32 start types, and the number behind InvalidValue is the one Windows reserves for leaving the setting alone.
| Member | Value | Win32 start type it is sent as |
|---|---|---|
InvalidValue | -1 | SERVICE_NO_CHANGE, the sentinel meaning “leave this alone” |
Automatic | 2 | SERVICE_AUTO_START |
Manual | 3 | SERVICE_DEMAND_START |
Disabled | 4 | SERVICE_DISABLED |
AutomaticDelayedStart | 10 | SERVICE_AUTO_START plus the delayed flag |
The same value is also the cmdlet’s internal marker for “the caller did not supply this parameter”: the write is guarded by a test that the startup type is not InvalidValue. So passing it explicitly is indistinguishable, inside the cmdlet, from not passing it at all.
The practical shape of this is a startup type coming from data rather than from a literal. A typo is caught at parameter binding and stops the script; InvalidValue is not. Reproducing the enum exactly as the product declares it and binding the same values against it measures both:
Name StartupType Bound Result
Spooler Manual ok WOULD WRITE dwStartType=3
wuauserv Disabled ok WOULD WRITE dwStartType=4
BITS InvalidValue ok SERVICE_NO_CHANGE - nothing written
W32Time Automatc ERR ParameterArgumentTransformationError
Spooler and wuauserv write. W32Time is a typo, throws at parameter binding and stops the script, which is the outcome you want. BITS returns success, writes nothing, and keeps whatever start type it already had. Nothing downstream can tell the BITS row and the Spooler row apart.
Set-Service did not throw” as “the service was changed”. It is not the same statement. The only reliable confirmation is to read the property back, which is what -PassThru is for.
An integer outside the member list behaves differently again. -StartupType 99 binds, because .NET converts any number to the underlying enum type, and then fails at the next step with the template “The startup type ‘{0}’ is not supported by {1}.”. That one you see.
One parameter name, two enums
-StartupType is spelled the same on Windows PowerShell 5.1 and on PowerShell 7, and it is backed by a different type in each. The reference pages state the type in the parameter’s own block, and the two member lists do not overlap completely.
| Windows PowerShell 5.1 | PowerShell 7.x | |
|---|---|---|
| Parameter type | System.ServiceProcess.ServiceStartMode | Microsoft.PowerShell.Commands.ServiceStartupType |
| Accepted values | Boot, System, Automatic, Manual, Disabled | Automatic, AutomaticDelayedStart, Disabled, InvalidValue, Manual |
| Default value | Automatic | none |
| Aliases | StartMode, SM, ST | StartMode, SM, ST, StartType |
Three things follow from that table and all three bite in practice. The first two are the members that exist on one side only: Boot and System are 5.1 only, and are documented there as valid for device drivers only, while AutomaticDelayedStart and InvalidValue are 7.x only.
The third consequence is the one that breaks working scripts, and it is about abbreviation. PowerShell converts a string to an enum member by unambiguous prefix, so on 5.1 Auto is enough to mean Automatic. On 7.x it is not, because AutomaticDelayedStart also begins with those four letters. Binding the same strings against both member lists:
value given 5.1 binds to 7.x binds to
Auto Automatic ERROR
Automatic Automatic Automatic
Boot Boot ERROR
System System ERROR
AutomaticDelayedStart ERROR AutomaticDelayedStart
InvalidValue ERROR InvalidValue
Set-Service -Name Spooler -StartupType Auto is the line most likely to be sitting in an old logon script or a build step. It is valid on Windows PowerShell 5.1 and it is a parameter binding error on PowerShell 7. Spell the value out in full and it works on both.
If you are moving scripts between the two, the upgrading to PowerShell 7 guide covers what else changes in the same jump.
Filtering on Status, and the prefix that matches nothing
Status is an enum too, System.ServiceProcess.ServiceControllerStatus, and the same prefix rule applies when you compare it against a string. That is why Where-Object Status -eq 'Running' works without any casting. It is also why a shorter string can silently match nothing.
The enum has seven members, and three of them begin with the letters St: Stopped, StartPending and StopPending. Filtering a four service sample on a range of prefixes gives this:
WHERE -eq 'Running' -> 1 match
WHERE -eq 'Runn' -> 1 match
WHERE -eq 'Run' -> 1 match
WHERE -eq 'R' -> 1 match
WHERE -eq 'Stopped' -> 2 matches
WHERE -eq 'Stop' -> 0 matches
WHERE -eq 'St' -> 0 matches
WHERE -eq 'S' -> 0 matches
WHERE -eq 'stopped' -> 2 matches
WHERE -eq 'RUNNING' -> 1 match
'R' resolves to exactly one member, so it works. 'Stop' matches three members, so it resolves to none of them and the row is treated as not equal. No error is written either way. The filter returns an empty set and the script reports that nothing is stopped.
Where-Object Status -eq 'Stop' never fires, because zero results and no error is exactly what a healthy machine looks like. Spell the status out in full, every time.
Case does not matter, as the last two rows show. Sort order does, and in a way the reference page itself gets tangled in. Sort-Object Status sorts on the underlying integers, where Stopped is 1 and Running is 4, so ascending puts stopped services first.
# stopped first, because the sort is on the enum's integer value, not its name
Get-Service | Sort-Object Status | Select-Object -First 5 -Property Status, Name
# running first
Get-Service | Sort-Object Status -Descending | Select-Object -First 5 -Property Status, Name
Get-Service reference page says both things about this in consecutive sentences. Its NOTES section states that “Stopped services appear before Running services”, then explains that “Running appears before Stopped because Stopped has a value of 1, and Running has a value of 4″. The reason supports the first sentence and contradicts the second. Example 7 on the same page shows stopped first, which settles it.
What the object carries, and what it does not
PowerShell 6.0 attached five properties to the returned object that plain .NET does not have. The Get-Service NOTES section lists them: UserName, Description, DelayedAutoStart, BinaryPathName and StartupType.
The Set-Service reference page disagrees with that list. Its -Description parameter states that “the Description isn’t a property of the Get-Service ServiceController object” and tells you to use Get-CimInstance instead. Two pages in the same module, two answers. The cmdlet source at v7.4.6 adds all five, including Description, so the Set-Service page looks like the stale one, but check it on your own build before you depend on it.
There is a third case that neither page mentions, and it is the one that produces a blank column on a machine where everything is fine. The five properties are attached only when Get-Service is called without -RequiredServices and without -DependentServices. Ask for a dependency list and you get bare .NET objects back:
# StartupType is populated here
Get-Service -Name Spooler | Select-Object Name, StartupType
# and blank here, on the same service, one switch apart
Get-Service -Name Spooler -RequiredServices | Select-Object Name, StartupType
The other way a column comes back empty is permissions. Filling those five properties means opening each service with SERVICE_QUERY_CONFIG, and when that fails the cmdlet writes a non-terminating error and still emits the object. The template is “Service ‘{1} ({0})’ cannot be queried due to the following error: {2}”, so a sweep across every service on a hardened machine emits one of those per service it cannot open and still hands back a result set you can work with.
Get-Service -Name Spooler | Get-Member lists what is actually there, and the answer differs between 5.1 and 7.x.
Running these against another machine
Neither cmdlet has a -ComputerName parameter any more. Both NOTES sections carry the same sentence: “Beginning in PowerShell 6.0, the command no longer includes the ComputerName parameter. To use this command on a remote computer, use the Invoke-Command to target a remote system.”
# the whole expression runs on the remote host, so the filter runs there too
Invoke-Command -ComputerName SRV-PROD-01 -ScriptBlock {
Get-Service | Where-Object { $_.StartType -eq 'Automatic' -and $_.Status -ne 'Running' }
}
There is a trap in the official example for this, and it is worth naming because the example is easy to copy without noticing. Example 7 on the Set-Service page fetches a service into $S locally, then passes $S inside an Invoke-Command script block. Local variables do not cross that boundary on their own: about_Remote_Variables states that “variables that are defined in a local session, must be identified as local variables in the command”, using the Using: modifier.
$using: fixes the scope but not the object. A ServiceController is a live handle to the local machine’s Service Control Manager, and it does not survive serialisation to another host. Look the service up on the far side, inside the script block, rather than shipping the object across.
Getting remoting itself working, and the errors it produces when it is not, is its own subject and it is not covered on this site yet.
Hidden gems
-DisplayName selects on one cmdlet and renames on the other. Get-Service -DisplayName '*update*' filters the service list by display name. Set-Service -DisplayName writes a new display name onto the service. The parameter is spelled identically and there is no confirmation prompt on either, so a -DisplayName pasted into the wrong cmdlet renames the Windows Update service instead of finding it.
Get-Service does not list drivers, but it will return one if you name it. The reference page splits the behaviour three ways: enumeration excludes device driver services, a wildcard returns only Windows services, and an exact match on a driver’s name returns that driver. So the same command that lists every service on the machine will happily return a kernel driver the moment you type its name. sc.exe query type= all is the form that enumerates both, and the driverquery article covers the driver side.
Restart-Service -Force is documented as a confirmation switch and is not one. Its reference page describes -Force as “forces the command to run without asking for user confirmation”. The cmdlet source declares the same parameter with the comment “specifies whether to force a service to stop even if it has dependent services” and passes it straight into the stop call. Restart-Service already has its own -Confirm parameter for confirmation, which is the giveaway.
The StartType alias collides with a property name. Set-Service accepts StartType as an alias for -StartupType, so -StartupType Manual and -StartType Manual are the same command. The returned object also has a StartType property, which is the .NET one and knows nothing about delayed start. Writing AutomaticDelayedStart and then reading StartType back is an easy way to convince yourself a delayed auto-start change did not apply.
gsv and spsv are the aliases worth knowing. gsv is Get-Service and spsv is Stop-Service. The Set-Service reference page lists no alias at all, so the write side has to be typed out in full.
The sc.exe and CIM equivalents
One setting, four places to read or write it, and it is not spelled the same way in all four. This is the table to keep open when you are translating a runbook from one to the other:
| Where you read or write it | Start automatically | Start on demand | Delayed |
|---|---|---|---|
Get-Service, StartType property | Automatic | Manual | reported as Automatic |
Get-Service, StartupType property | Automatic | Manual | AutomaticDelayedStart |
Get-CimInstance Win32_Service, StartMode property | Auto | Manual | Auto plus DelayedAutoStart |
sc.exe config start= | auto | demand | delayed-auto |
The CIM class is also where the fields ServiceController never had live. PathName is documented as the “fully qualified path to the service binary file”, StartName as the “account name under which a service runs”, and ProcessId gives you the running process, which Get-Service does not expose at all.
# the columns Get-Service has no property for, including the PID
Get-CimInstance -ClassName Win32_Service -Filter "Name = 'Spooler'" |
Select-Object Name, StartMode, State, ProcessId, StartName, PathName |
Format-List
That ProcessId is the bridge to the process side: feed it to Get-Process -Id or to the tasklist and taskkill pair and you are looking at the actual worker, which for a shared-process service is one svchost.exe among many.
For the CMD side of all this, sc.exe remains the tool that reads and writes the same settings without PowerShell, and the sc command article covers its query and config syntax including the space that has to follow every equals sign.
Where this matters
Post-patch verification. One filter over every automatic service that is not running tells you in seconds whether a reboot came back clean, which is faster than any console and scriptable across a fleet.
Hardening a build image. Setting a list of services to disabled is a Set-Service loop, and the InvalidValue trap is exactly the kind of silent no-op that leaves a golden image shipping with a service you thought you had turned off.
Migrating scripts to PowerShell 7. The Auto abbreviation and the Boot value are two of the few genuinely breaking changes in a service script, and both fail loudly at binding time rather than quietly.
Incident triage. Reading Status and StartType in one command tells you whether a service is stopped because it crashed or because somebody disabled it, and those are different incidents.
Auditing service accounts. The logon account is the field that turns a service inventory into a security review, and it comes from Get-CimInstance Win32_Service on 5.1 and from either source on 7.x.
Tips and limitations
- Both cmdlets are Windows only. On Linux and macOS builds of PowerShell 7 they are not present at all.
Set-Servicerequires an elevated session;Get-Servicedoes not. The failure is a non-terminating error, not a prompt.Set-Servicewrites no output unless you add-PassThru. Silence is not confirmation.Set-Serviceaccepts one service name at a time. Pipe multiple services into it to configure several.- Wildcards work in
-NameonGet-Serviceand are not permitted in-NameonSet-Service. -ForceonSet-Serviceonly takes effect together with-Status Stopped. It does nothing on a startup type change.- Comparing
-eqor-neagainst an enum property resolves the string by prefix, so full member names are safer than abbreviations everywhere. - The five added properties come from an extra query per service, so a full sweep does more work than the default view and can emit one error per service it cannot open.
Set-Service -Descriptionchanges the service description. The two reference pages disagree about whetherGet-Servicecan read it back, so check your own build rather than assuming.- Nothing here removes a service.
Remove-Servicedoes that, and like the rest of the family it is Windows only and needs an elevated session.
Official documentation
- Get-Service: PowerShell | Microsoft Learn
- Set-Service: PowerShell | Microsoft Learn
- Stop-Service: PowerShell | Microsoft Learn
- Restart-Service: PowerShell | Microsoft Learn
- about_Remote_Variables: PowerShell | Microsoft Learn
- Win32_Service class: Win32 apps | Microsoft Learn
- sc.exe config: Windows Commands | Microsoft Learn
Related tools
- Event Log Analyzer: a service that fails to start writes the reason to the System event log, and this is where you read it after
Get-Servicetells you what state the service is in.
Related guides
- sc command in Windows: the CMD tool that reads and writes the same service settings, including the query filters PowerShell has no equivalent for.
- Upgrading to PowerShell 7: the jump that changes which values
-StartupTypewill accept, and what else moves with it. - tasklist and taskkill in Windows: what to do with the process ID once CIM hands you the worker behind a shared-process service.
- driverquery in Windows: the other half of the driver and service split that
Get-Serviceenumeration leaves out. - Get-WinEvent and PowerShell event logs: reading the System log entries a failed service start leaves behind.
- runas and UAC token filtering: why an elevated session is needed before
Set-Servicewill write anything.
Five cheat sheets, one PDF
Subnet masks, PowerShell, Linux commands, HTTP status codes and the ESXi command line - one page each, free to keep. Leave an address and it arrives in a minute.