Flamingo Raises $4.5M Seed Round

Every PowerShell list on the internet is sorted alphabetically. That's fine when you already know the name of the command you want, and no help at 4pm with a user on the phone whose laptop sounds like a hairdryer. This one is sorted by the ticket instead: the machine is slow, the disk is full, the service died, the share won't mount. Each command is one line you can paste into a console, with what it hands back and where it bites. Version differences between Windows PowerShell 5.1 and PowerShell 7 are flagged where they'll catch you.

TL;DR

  • Start with three. Get-Help, Get-Command and Get-Member will teach you every other command on this page.
  • Daily triage. Get-Process, Get-Volume and Get-WinEvent cover the slow-machine, full-disk and dead-service tickets.
  • Network. Test-NetConnection <host> -Port <n> replaces ping plus telnet, with no UDP support.
  • At scale. Invoke-Command runs any of it across many machines at once, if WinRM is on.
  • Before you change anything. Run the Get- version first, then add -WhatIf.

Every One-Liner in One Table

If you came looking for a PowerShell commands cheat sheet, this table is it. The sections after it explain each line and where it bites.

What you needThe commandThe catch
Top CPU processesGet-Process | Sort CPU -Desc | Select -First 10CPU is cumulative seconds, not percent
Real-time CPU %(Get-Counter '\Process(*)\% Processor Time').CounterSamplesCounter names are localized
Top memoryGet-Process | Sort WS -Desc | Select -First 10Working set over-sums shared pages
Uptime(Get-Date) - (Get-CimInstance Win32_OperatingSystem).LastBootUpTimeGet-Uptime is PS 6+ only
Startup itemsGet-CimInstance Win32_StartupCommandMisses scheduled tasks
Free spaceGet-Volume | Where DriveLetterWindows-only module
Biggest filesGet-ChildItem C:\ -Recurse -File | Sort Length -DescMinutes on a big volume
Clear tempGet-ChildItem $env:TEMP -Recurse | Remove-Item -Recurse -Force$env:TEMP is per-user
Stopped auto servicesGet-CimInstance Win32_Service -Filter "StartMode='Auto' AND State='Stopped'"Auto in CIM, Automatic in .NET
Restart a serviceRestart-Service -Name Spooler -Force-Force means "don't confirm"
Process behind a serviceGet-CimInstance Win32_Service | Select Name, ProcessIdMany services share one PID
Unexpected shutdownsGet-WinEvent -FilterHashtable @{LogName='System'; Id=6008,41,1074}Filter in the hashtable, not after
Service crashesGet-WinEvent -FilterHashtable @{LogName='System'; Id=7034}Get-EventLog is gone in PS 6+
Failed logonsGet-WinEvent -FilterHashtable @{LogName='Security'; Id=4625}Needs elevation or Event Log Readers
Port testtnc <host> -p <port>No UDP
IP configGet-NetIPConfiguration -AllDefault hides disconnected adapters
Flush DNSClear-DnsClientCacheAll or nothing
Listening portsGet-NetTCPConnection -State ListenIt's Listen, not Listening
Local adminsGet-LocalGroupMember -Group 'Administrators'Breaks on unresolvable SIDs
Installed softwareGet-ItemProperty 'HKLM:\...\Uninstall\*'Never use Win32_Product
PatchesGet-HotFix | Sort InstalledOn -DescCBS updates only
Run it everywhereInvoke-Command -ComputerName $list -ScriptBlock {...}WinRM is off on client Windows

Triage a Machine That's Crawling

The first thing to know about Get-Process is that its CPU column answers a different question than the one you're asking.

powershell
Get-Process | Sort-Object CPU -Descending | Select-Object -First 10 Name, Id, CPU, WS

Microsoft defines it as "the amount of processor time that the process has used on all processors, in seconds." Cumulative, since the process started. A Chrome window open since Monday outranks whatever is pinning a core right now: useful for finding a long-running hog, misleading for "why is this slow now."

For the instantaneous number, you want performance counters:

powershell
(Get-Counter '\Process(*)\% Processor Time').CounterSamples |
  Sort-Object CookedValue -Descending | Select-Object -First 10 InstanceName, CookedValue

Filter out _Total and Idle, which sort straight to the top, and expect values over 100% on a multi-core box since they sum across cores. Two catches: counter names are localized, so this breaks on non-English Windows unless you look up local names with Get-Counter -ListSet, and Get-Counter was missing from 6.x entirely.

Memory, same shape:

powershell
Get-Process | Sort-Object WS -Descending |
  Select-Object -First 10 Name, Id, @{n='WS(MB)';e={[int]($_.WS/1MB)}}

Working set isn't committed memory, and shared pages count against every process touching them, so the column over-sums. It still points at the right process.

Two more that close tickets on their own:

powershell
(Get-Date) - (Get-CimInstance Win32_OperatingSystem).LastBootUpTime
Get-CimInstance Win32_StartupCommand | Select-Object Name, Command, Location, User

The first verifies "have you tried turning it off and on again" rather than asking. Get-Uptime is tidier but arrived in PowerShell 6, so the CIM version works everywhere. The second covers Run and RunOnce keys plus both Startup folders, but not scheduled tasks, where the genuinely annoying stuff hides now.

Find Out Where the Disk Went

powershell
Get-Volume | Where-Object DriveLetter |
  Select-Object DriveLetter, FileSystemLabel,
    @{n='FreeGB';e={[math]::Round($_.SizeRemaining/1GB,1)}},
    @{n='SizeGB';e={[math]::Round($_.Size/1GB,1)}}

The Where-Object drops recovery and EFI partitions, which have no drive letter. Then find the weight:

powershell
Get-ChildItem C:\ -Recurse -File -ErrorAction SilentlyContinue |
  Sort-Object Length -Descending | Select-Object -First 20 FullName, Length

Length is bytes. -File matters more than it looks, because Length on a directory object throws. Expect minutes on a large volume with no output until it finishes: Sort-Object buffers every object in memory before returning the first one.

Per-folder totals, usually what you want on a user profile:

powershell
Get-ChildItem C:\Users -Directory | ForEach-Object {
  [pscustomobject]@{
    Path   = $_.FullName
    SizeGB = [math]::Round((Get-ChildItem $_.FullName -Recurse -File -ErrorAction SilentlyContinue |
             Measure-Object Length -Sum).Sum/1GB, 2)
  }
} | Sort-Object SizeGB -Descending

One 5.1 difference: from PowerShell 7.3, Measure-Object stopped erroring on objects missing the property you asked for. On 5.1 it throws, so that inner -ErrorAction SilentlyContinue is load-bearing.

Clearing temp:

powershell
Get-ChildItem $env:TEMP -Recurse | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue

Written that way for a documented reason: Remove-Item's own -Recurse has a known issue, and Microsoft's docs work around it with exactly this pipe. The trap catches everyone once: $env:TEMP is per-user, so running it elevated cleans the admin's temp folder and leaves the user's untouched.

Work Out Why a Service Stopped

Start with the trap, because it returns zero rows and no error, and sends you hunting a problem that was never there. WMI's Win32_Service.StartMode calls auto-start Auto. The .NET enum behind Get-Service calls it Automatic. Match the wrong word to the wrong source and you get a clean, empty, completely wrong answer.

powershell
# PowerShell 7
Get-Service | Where-Object { $_.StartType -eq 'Automatic' -and $_.Status -eq 'Stopped' }

# works everywhere, and faster
Get-CimInstance Win32_Service -Filter "StartMode='Auto' AND State='Stopped'" |
  Select-Object Name, DisplayName, StartMode, State

The CIM version filters server-side instead of pulling back every service and discarding most of them. Before you fix what it returns: delayed-start services show Stopped for a while after boot, and trigger-start services sit Stopped by design. Neither is broken.

powershell
Restart-Service -Name Spooler -Force

-Force here means "don't ask me to confirm," not "handle dependent services." That behaviour belongs to Stop-Service -Force. If something has running dependents, stop it with Stop-Service -Force and start it again as two steps.

And the join that explains why killing one process took half the machine with it:

powershell
Get-CimInstance Win32_Service -Filter "State='Running'" | Where-Object ProcessId |
  Select-Object Name, ProcessId,
    @{n='Process';e={(Get-Process -Id $_.ProcessId -ErrorAction SilentlyContinue).Name}}

ProcessId is 0 for anything stopped. Many services share one host process, so this is many-to-one by design: Win32_Service's ServiceType tells you which is which, with Own Process and Share Process as the values that matter.

Read the Event Log Without Drowning In It

Get-EventLog is gone: dropped in PowerShell 6, absent from every 7.x build, because it leaned on unsupported APIs. Microsoft discourages it even on 5.1, where its own docs call the underlying Win32 API deprecated and warn "the results may not be accurate." Learn Get-WinEvent instead.

powershell
Get-WinEvent -FilterHashtable @{LogName='System'; Id=6008,41,1074; StartTime=(Get-Date).AddDays(-7)}

The unexpected-shutdown trio: 6008 (dirty shutdown), 41 (kernel power, lost power without a clean stop), 1074 (something asked for the restart, and it names what).

Why the hashtable rather than piping into Where-Object? The docs are blunt: filters "are applied as the objects are retrieved," while Where-Object "retrieves all of the objects, then applies filters to all of the objects." On a Security log with half a million entries that's two seconds versus a coffee break.

powershell
Get-WinEvent -FilterHashtable @{LogName='System'; ProviderName='Service Control Manager'; Id=7034} -MaxEvents 20
Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4625; StartTime=(Get-Date).AddDays(-1)} -MaxEvents 50

7034 is a service terminating unexpectedly. 4625 is a failed logon, and it turns "my account keeps locking out" into an answer. Read the status code: 0xC000006A bad password, 0xC0000064 bad username (a run of those is someone guessing at account names), 0xC0000072 disabled, 0xC0000234 already locked. Logon type 3 is network, type 10 is RDP.

The Security log needs elevation or membership in Event Log Readers, and here's the nasty part: a permissions failure returns the same "no events found" message as a genuinely empty result. An unelevated query looks exactly like good news.

Test the Network Path in One Line

Test-NetConnection was the highest-voted answer in the r/sysadmin thread that still ranks on page one for this topic, and the reply underneath it explained why: "It's an essential command that surprisingly few people seem to know."

powershell
tnc mail.contoso.com -p 443
(Test-NetConnection 10.0.0.5 -Port 3389).TcpTestSucceeded

tnc is the alias, -ComputerName is positional so you can drop it, and -p works for -Port. The second form returns a bare True or False for scripts.

The limitation to know before you lose an hour: no UDP. For that you're back to netcat or a capture.

powershell
Get-NetIPConfiguration -All
Clear-DnsClientCache
Resolve-DnsName mail.contoso.com -Type MX -Server 8.8.8.8
Get-NetTCPConnection -State Listen |
  Select-Object LocalAddress, LocalPort, OwningProcess,
    @{n='Process';e={(Get-Process -Id $_.OwningProcess).ProcessName}} | Sort-Object LocalPort

-All matters on anything running Hyper-V or a VPN client, because the default shows connected physical adapters only and hides the one causing the problem. Clear-DnsClientCache is exactly ipconfig /flushdns.

The last one replaces netstat -anb. The state is Listen, not Listening, which everyone gets wrong once. netstat -b does the same job, but Microsoft warns it "can be time-consuming and will fail unless you have sufficient permissions." The PowerShell version pulls process names from unprivileged data and hands back sortable objects.

Check Users, Sessions and Local Admins

powershell
Get-LocalGroupMember -Group 'Administrators' | Select-Object Name, ObjectClass, PrincipalSource
quser

PrincipalSource is the column that earns its place: it tells you whether each admin is Local, Active Directory, a Microsoft Entra group or a Microsoft Account. That's the answer to "who can elevate on this box."

Recognize this rather than debugging it: Failed to compare two elements in the array. It fires when the group holds a SID that won't resolve, usually an Entra SID, an Intune enrollment account, or a machine moved out of a domain. It hits 5.1 as well as 7, and Microsoft closed the tracking issues without a fix. Fall back to net localgroup Administrators.

On PowerShell 7 the LocalAccounts module runs only through the compatibility shim, which hands back serialized copies rather than live objects, so methods on the returned user objects won't fire. That's behind the "works in 5.1, breaks in 7" reports on this module.

See What's Installed and What's Patched

Start with the one to never run:

powershell
Get-WmiObject Win32_Product        # don't

Microsoft's own class documentation explains why, and it's worse than slow: querying it "initiates a consistency check of packages installed, verifying and repairing the install." Every MSI application on the machine gets reconfigured, event 1035 firing once per app, plus 11708 failures under a non-admin account. There's a Microsoft KB called Windows Installer reconfigured all applications that exists because of this. It's incomplete too, since it only sees MSI-installed software.

The registry answers the same question without touching anything:

powershell
Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*',
                 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*',
                 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*' -ErrorAction SilentlyContinue |
  Where-Object DisplayName |
  Select-Object DisplayName, DisplayVersion, Publisher, UninstallString

Three paths, because 32-bit apps on 64-bit Windows land in WOW6432Node and per-user installs land in HKCU. Where-Object DisplayName drops patch entries and orphans. Don't trust InstallDate: Microsoft documents it as "the last time this product received service," so every patch rewrites it, and plenty of non-MSI installers never write it at all.

powershell
Get-HotFix | Sort-Object InstalledOn -Descending | Select-Object -First 10
winget list --upgrade-available
winget upgrade --all --include-unknown

Get-HotFix is narrower than it looks: the docs are explicit that it returns only Component Based Servicing updates, and that "updates supplied by Microsoft Windows Installer (MSI) or the Windows Update site aren't returned." On Windows 10 and 11 cumulative updates supersede each other, so a short list is normal and it is not a patch history.

--include-unknown isn't optional in practice, because anything whose installed version winget can't read gets skipped silently without it. Microsoft also publishes Microsoft.WinGet.Client, stable at 1.29.280 as of July 2026, which returns objects instead of text: Get-WinGetPackage | Where-Object IsUpdateAvailable. PSWindowsUpdate, the third-party module everyone reaches for, is still at 2.2.1.5 from July 2024, isn't Microsoft-supported, and is blocked from remote sessions by a Windows Update Agent restriction.

The Three Commands That Teach You the Rest

Thotaz, in that same r/sysadmin thread, pushed back on this entire genre of post: "Don't try to learn random oneliners... Instead of that you should put in the effort to learn the basic syntax and mechanics of PowerShell." Fair enough. Here's the short path.

powershell
Get-Command -Noun *Service*          # what exists
Get-Help Get-Service -Examples       # how it's used
Get-Service | Get-Member             # what came back, and what it can do

Get-Member is the one experienced people kept naming, with aMazingMikey putting it plainly: "If you want to really understand PowerShell, Get-Member. Pipe to it." Pipe into it rather than using -InputObject: piping a collection gets you the members of each object, -InputObject gets you the members of the collection.

Two notes. -Verb and -Noun can't be combined with -Name, so it's Get-Command -Noun *Service* or Get-Command -Name *Service* -CommandType Cmdlet, never both. And if Get-Help returns nothing but syntax, run Update-Help: Windows ships without help files, which needs elevation on 5.1 but defaults to CurrentUser scope from PowerShell 6.1.

Mistakes Worth Skipping

These aren't style preferences. Each is encoded as a rule in PSScriptAnalyzer, Microsoft's own linter.

Format-Table ends the pipeline. Anything downstream gets nothing usable. The mechanism is documented rather than folklore: Format-Table returns format objects of type Microsoft.PowerShell.Commands.Internal.Format, so past that point you're piping rendering instructions instead of your data. Format last, always.

Aliases belong in the console, not in scripts. The cleanest statement of this came out of the r/sysadmin thread and deserves to be the rule everywhere: "Gci goes in the blue window. Get-childitem goes in the white window." Type gci interactively all you like. In something another tech inherits, write it out.

-ErrorAction SilentlyContinue on everything. jeroen-79 described the pattern as an infomercial: "Senior techs hate him! See how this junior gets work done without errors by simply adding '-ErrorAction SilentlyContinue' to all his commands." Use it where you expect and understand the noise, like a recursive walk hitting folders you can't read. Don't use it to make red text go away. Suppressed errors still land in $Error, so you can check afterwards.

Skipping the Get- first. LetMeAskPls put the discipline in one line: always do a GET before you do a SET. Read the current value, change it, read it back to confirm it landed where you expected. Two extra commands, and it's the difference between a change and a guess.

Run It Across the Fleet, Not One Machine at a Time

Everything above assumes one machine. The job usually isn't.

powershell
Invoke-Command -ComputerName (Get-Content .\servers.txt) -ThrottleLimit 32 -ScriptBlock { $PSVersionTable.PSVersion }

Thirty-two machines at a time by default, and it carries the best war story in the thread. ArmedwWings, during the CrowdStrike outage: "I was able to use invoke command to delete the trouble file in the 3-5 seconds the computers were up before crashing." Milkshakes00 did the same with a ping loop: "when it returned a connection I started blasting it with remove-item."

For repeated work against the same boxes, hold a session and pass local variables in with $using::

powershell
$s = New-PSSession -ComputerName SRV01, SRV02 -Credential (Get-Credential)
Invoke-Command -Session $s -ScriptBlock { Get-WinEvent -LogName $using:Log -MaxEvents 10 }
Remove-PSSession $s

Remove them when you're done, because sessions hold server-side resources against a WinRM quota that defaults to five shells per user.

Four things bite at scale.

Remoting is off on client Windows. On by default from Server 2012, disabled on every workstation. Enable-PSRemoting fails on public networks unless you add -SkipNetworkProfileCheck, and even then it only opens the local subnet. StaticVoidMain2018's reply on r/sysadmin: "Never been in an org where psremoting is enabled."

Version drift. 5.1 is the only version guaranteed present, which is why calculatetech, running Datto and Panda, told r/msp that "everything must be written in PowerShell 2.0 for Windows or you'll hit compatibility issues." On PowerShell 7, Enable-PSRemoting creates PowerShell.7 endpoints while default remoting still lands on the 5.1 one unless you name the configuration.

SYSTEM context. RMM script engines mostly run as SYSTEM, which BuldozzerMSP flagged on r/msp: "my RMM (Datto) usual runs scripts as the system user." That breaks the HKCU half of the software inventory above, and winget too, since it registers per user.

-WhatIf isn't everywhere. Each cmdlet has to opt in, so check with (Get-Command Restart-Service).Parameters.Keys -contains 'WhatIf'. brokerceej's answer on r/msp is worth stealing wholesale: build your own. "Every script I write has a -DryRun parameter and comprehensive transcript based output to a logfile." Acronis Threat Research Unit, looking at 100 million script runs across 11,500 organizations in May 2026, found 97% of scripts take no parameters at all and 85% get abandoned after being written. A one-liner hardcoded to one machine is one nobody reuses.

At some point the console stops being the right place for this. Running a one-liner against four hundred endpoints needs an inventory of what's out there, a way to reach machines that are asleep or off the network, and a record of what ran where and what came back. That's what a script engine inside an RMM is for.

OpenFrame is ours, and what's worth knowing here is the shape of it rather than a feature list: open source, $1 per device per month, with RMM, patching, remote access, documentation and SIEM in one place instead of four contracts and four agents on every endpoint. The AI agents in it, Fae and Mingo, do ticket work inline rather than suggesting it to a tech, so the routine diagnostics and remediation clear themselves and your team gets those hours back for the work that needs a human. The one-liners on this page are the kind of thing they run.

Where to Go Next

Keep this open as a PowerShell commands cheat sheet if that's useful, but the actual upgrade is Get-Command, Get-Help -Examples and Get-Member. Those three turn any unfamiliar module into something you can work out alone, which no list can do for you.

Two things worth doing this week. Get a -DryRun switch into every script you write before it goes anywhere near a client, because -WhatIf won't be there when you need it most. And if you're running these one machine at a time across client sites, that's the signal to move the work into a script engine.

Our breakdown of IT automation tools picks up where the console stops.

Conrad Lunderstedt

Conrad Lunderstedt

Solution Architect

I'm Conrad, Solution Architect at Flamingo. Before this I spent the better part of twenty years in IT and managed services, a lot of it sitting next to technicians while they tried to make software do what the brochure said it would. Now I spend my days talking with MSPs about the stack they already run, and helping them work through the requests and issues that come with it.

Related Content

Blog Posts

Product Releases

Podcasts

Webinars

Case Studies

Events

Onboarding Guides

Frequently Asked Questions

PowerShell

Three teach you the rest: Get-Command finds what exists, Get-Help with -Examples shows how it is used, and Get-Member shows what an object came back with. Past those, the daily set is Get-Process for a slow machine, Get-Volume for disk space, Get-Service for something that stopped, Get-WinEvent for why it stopped, and Test-NetConnection for a blocked port.
Get-Command lists everything available in your session, and you can narrow it with Get-Command -Noun *Service* or Get-Command -Module NetTCPIP. One trap: -Verb and -Noun sit in a different parameter set from -Name, so you can use one form or the other, never both in the same call.
Because the two sources spell it differently. Win32_Service in WMI and CIM calls auto-start "Auto", while the .NET enum behind Get-Service calls it "Automatic". Match the wrong word to the wrong source and you get zero rows and no error at all. Use Get-CimInstance Win32_Service with -Filter "StartMode='Auto'", or Get-Service filtered on StartType -eq 'Automatic'.
No. Microsoft documents that column as the processor time a process has used across all processors, in seconds, totalled since it started. A browser open all day outranks whatever is pinning a core right now. For the instantaneous figure use Get-Counter with the '\Process(*)\% Processor Time' path, and filter out _Total and Idle, which sort straight to the top.
No. It was removed in PowerShell 6 and is absent from every 7.x build, because it relied on unsupported APIs. Microsoft discourages it on Windows PowerShell 5.1 too, where the docs call the underlying Win32 API deprecated and warn the results may not be accurate. Use Get-WinEvent with -FilterHashtable instead, which also filters as records are read rather than after.
Querying that class triggers a consistency check that verifies and repairs every MSI package on the machine. You get event 1035 once per installed application, and 11708 installation failures under a non-admin account. It is incomplete as well, since it only ever sees MSI-installed software. Read the registry Uninstall keys instead, which touch nothing.
Mostly, with edges that bite. Get-EventLog and the -ComputerName parameter are gone from several cmdlets, the LocalAccounts module runs only through a compatibility shim that hands back serialized objects rather than live ones, and Get-Counter was missing from 6.x entirely. Windows PowerShell 5.1 is still the only version guaranteed present on a client endpoint.
Invoke-Command with -ComputerName takes a list and runs the script block against 32 machines at a time by default. It needs WinRM, which is enabled on Windows Server from 2012 onward but disabled on client workstations. For repeated work against the same machines, hold a session with New-PSSession and pass local variables in with the $using: prefix.

About OpenFrame

OpenFrame isn't built to plug into your stack. It replaces it. Instead of duct-taping a dozen tools together (RMM, MDM, SIEM, patching, remote access, each its own login and bill), we bundle it into one unified platform: RMM, MDM, monitoring, automation, remote access, patch management, security monitoring, and ticketing, plus built-in AI copilots. So "does it integrate with X?" usually means: you won't need X anymore.
Most platforms give you one piece and expect you to bolt the rest on. OpenFrame unifies the whole stack in one place, with AI copilots built in. Fewer logins, fewer bills, less duct tape.