1. Environments & Versions
Before writing scripts, you need to know where to write them and what version you are running.
PowerShell 5.1 vs PowerShell 7
There are two distinct families of PowerShell today:
- Windows PowerShell 5.1: This is the legacy version built directly into Windows 10 and 11. It is accessible via the blue
powershell.exewindow. Microsoft is no longer adding new features to 5.1, but it remains the bedrock of Windows administration because it is guaranteed to exist on every endpoint. - PowerShell 7 (Core): This is the modern, open-source, cross-platform version of PowerShell (it runs on Windows, macOS, and Linux). It is accessible via the black
pwsh.exewindow. It is significantly faster and has modern language features, but you must download and install it manually.
Which should I use?
Write scripts in PowerShell 7 for your own daily administration, but test your scripts in PowerShell 5.1 if you plan to deploy them to end-user laptops via Intune (since Intune uses 5.1 by default).
The Code Editors (IDE)
You can type commands directly into the terminal window, but when writing multi-line scripts, you need an editor.
- PowerShell ISE: The "Integrated Scripting Environment" is built into Windows. It provides a split-screen view with a script editor on top and a terminal on the bottom. While classic, Microsoft has deprecated it and it cannot run PowerShell 7.
- Visual Studio Code (VS Code): The undisputed modern standard. VS Code is a free, lightweight editor by Microsoft. By installing the "PowerShell Extension" in VS Code, you get deep IntelliSense (autocomplete), debugging, formatting, and full support for both PowerShell 5.1 and 7. This is what you should be using.
2. Discovery & Objects
Welcome to PowerShell. If you are coming from Command Prompt (CMD) or Linux Bash, the most important thing to learn is this: PowerShell does not output text, it outputs Objects.
An object is a data structure that contains properties (information) and methods (actions). When you run a command to get a user, you aren't getting a string of text that says "John Doe". You are getting a User Object that contains a FirstName property, a LastName property, an IsActive property, etc.
Self-Discovery: The Core Three Commands
You don't need to memorize PowerShell commands. You just need to know how to ask PowerShell for help. The three most important commands in PowerShell are Get-Command, Get-Help, and Get-Member.
Finding Commands
Use Get-Command to find commands based on verbs (like Get, Set, New, Remove) or nouns (like User, Mailbox, Device).
# Find all commands related to a Mailbox
Get-Command -Noun Mailbox
# Find all commands that "Set" something regarding a User
Get-Command -Verb Set -Noun *User*
Learning How to Use a Command
Once you find a command, use Get-Help to understand its syntax. Adding -Examples shows you exactly how to type it.
# Read the instruction manual for New-Mailbox
Get-Help New-Mailbox
# See real-world examples of how to use it
Get-Help New-Mailbox -Examples
Exploring Objects
Use Get-Member to see what properties and methods are hidden inside an object.
# See all the properties attached to a process
Get-Process | Get-Member
3. Variables & Data Types
Variables act as containers to store information so you can use it later. In PowerShell, variables always start with a dollar sign ($).
Basic Variables
PowerShell automatically guesses the data type, but you can force it if necessary.
# A String (Text)
$userName = "Admin User"
# An Integer (Whole Number)
$retryCount = 5
# A Boolean (True/False)
$isActive = $true
Arrays (Lists)
An array is a list of items stored in a single variable. Arrays are created using the @() syntax.
# Create a list of email addresses
$emailList = @("user1@domain.com", "user2@domain.com", "user3@domain.com")
# Access the first item in the array (Computers start counting at 0)
Write-Output $emailList[0] # Outputs: user1@domain.com
Hash Tables (Dictionaries)
Hash tables store "Key-Value" pairs. They are incredibly useful for pairing data together, like a username and their department. They use the @{} syntax.
# Create a Hash Table
$userDepartments = @{
"user1@domain.com" = "Sales"
"user2@domain.com" = "Marketing"
}
# Lookup a specific value
Write-Output $userDepartments["user1@domain.com"] # Outputs: Sales
4. The Pipeline
The Pipeline (|) is the heart of PowerShell. It takes the output of the command on the left, and passes it directly as the input to the command on the right.
Because PowerShell passes objects, the receiving command understands all the properties of the data being passed to it.
Filtering with Where-Object
You can use the pipeline to pass a massive list of data into Where-Object, which acts like a filter. It only lets objects through if they match your criteria. Inside the filter, $_ represents the current object being looked at.
# Get all processes, but ONLY keep the ones using more than 100MB of RAM
Get-Process | Where-Object { $_.WorkingSet -gt 100MB }
# Get all mailboxes, filter for only the Shared ones
Get-Mailbox | Where-Object { $_.RecipientTypeDetails -eq "SharedMailbox" }
Formatting with Select-Object
Select-Object is used to grab only the specific properties you care about, creating a clean table of output.
# Get all mailboxes, but only show their Display Name and Primary Email
Get-Mailbox | Select-Object DisplayName, PrimarySmtpAddress
5. Logic & Loops
Scripts become powerful when they can make decisions and perform repetitive tasks automatically.
If / ElseIf / Else
Use If statements to run code only when certain conditions are met. Note that PowerShell uses operators like -eq (equal), -ne (not equal), -gt (greater than), and -lt (less than) instead of standard math symbols.
$userStatus = "Active"
if ($userStatus -eq "Active") {
Write-Output "The user can log in."
} elseif ($userStatus -eq "Suspended") {
Write-Output "The user is suspended."
} else {
Write-Output "Status unknown."
}
The ForEach Loop
The foreach loop is used to iterate through an array of items one by one. This is essential for bulk processing, such as reading a CSV file and creating 100 users at once.
$servers = @("Server01", "Server02", "Server03")
foreach ($server in $servers) {
Write-Output "Checking connection to $server..."
Test-NetConnection -ComputerName $server -Port 443
}
6. Modules & Policies
PowerShell out-of-the-box only knows how to manage Windows. To manage Exchange, Intune, or VMware, you must install additional "instruction manuals" called Modules.
Execution Policies
Before you can run custom scripts, you must configure the Windows Execution Policy. By default, it is set to "Restricted" to prevent malicious scripts from executing.
The standard best practice for IT admins is RemoteSigned. This means local scripts you write yourself will run without issue, but scripts downloaded from the internet must be signed by a trusted publisher.
# Must be run in an Administrator PowerShell window
Set-ExecutionPolicy RemoteSigned -Scope CurrentUser
Installing from the PowerShell Gallery
The PSGallery is Microsoft's official repository for modules. You can find, install, and update modules directly from the command line.
# Install the Exchange Online management module
Install-Module -Name ExchangeOnlineManagement -Force -AllowClobber
# Import the module into your current session so you can use its commands
Import-Module ExchangeOnlineManagement
7. Microsoft Graph API
Historically, admins used the AzureAD or MSOnline modules to manage Office 365. These are now deprecated. The modern standard is the Microsoft Graph PowerShell SDK.
Mind the Scopes
When you ran Connect-AzureAD, you logged in with your Global Admin account and could do anything. Graph doesn't work like that. Graph requires you to request specific Scopes (Permissions) when connecting, ensuring your script only has the minimum access required to do its job.
Connecting with Scopes
If you want to read user data, you must request User.Read.All. If you want to modify users, you must request User.ReadWrite.All.
# Connect to Graph requesting permission to read/write users and read groups
Connect-MgGraph -Scopes "User.ReadWrite.All", "Group.Read.All"
Finding the Right Scope
Because there are hundreds of scopes, it can be hard to know which one you need. Microsoft provides a tool called Find-MgGraphCommand to help.
# Ask Graph what permissions you need to run the "Update-MgUser" command
Find-MgGraphCommand -Command Update-MgUser | Select-Object -ExpandProperty Permissions
8. Intune Proactive Remediations
Proactive Remediations are a powerful feature of Microsoft Intune that allows you to deploy scripts to endpoints to automatically detect and resolve common issues before users submit a helpdesk ticket.
The Two-Script System
A remediation package requires two scripts:
- Detection Script: Runs on a schedule. It checks the computer for a specific problem (e.g., Is a required registry key missing?).
- Remediation Script: Only runs if the Detection Script reports a problem. Its job is to fix the issue.
The Importance of Exit Codes
Intune does not read your screen. It communicates with your script exclusively through Exit Codes. If you do not explicitly state an exit code, Intune may misinterpret the result.
Exit 0: Success / Compliant (No action needed, or fix succeeded).Exit 1: Failure / Non-Compliant (Problem detected, or fix failed).
Writing a Detection Script
In this example, we check if the Windows Print Spooler service is running. We use Write-Output so the text shows up in the Intune console logs.
try {
$service = Get-Service -Name Spooler
if ($service.Status -eq 'Running') {
Write-Output "Spooler service is running properly."
Exit 0 # Tell Intune the device is Compliant
} else {
Write-Output "Spooler service is stopped."
Exit 1 # Tell Intune it is Non-Compliant. This triggers the Remediation script.
}
} catch {
Write-Output "Error querying service."
Exit 1
}
Writing a Remediation Script
The remediation script only fires if the detection script exited with 1. Its goal is to resolve the problem and exit with 0 if successful.
try {
Start-Service -Name Spooler -ErrorAction Stop
Write-Output "Successfully started the Print Spooler service."
Exit 0 # Tell Intune the fix was successful
} catch {
Write-Output "Failed to start the Print Spooler service. Manual intervention required."
Exit 1 # Tell Intune the fix failed
}