Build a System Health Monitor
Write a PowerShell script that queries Windows and generates an HTML report.
Introduction
30 Second Summary
When your computer slows to a crawl or a critical service stops running, the first question is always the same: what is actually happening on this machine right now? System administrators answer that question dozens of times a day.
In this project, you will build a PowerShell script that checks your Windows machine's health and generates a professional HTML report. You will query live data like disk space, top processes, and stopped services, then transform it into something you can open in a browser and show in an interview.
What You'll Build
You run one command in your terminal, and your default browser pops open with a styled dashboard showing your machine's OS version, disk usage, heaviest processes, and any services that have unexpectedly stopped.
By the end of this project, you'll have:
- A single-command reporting tool that queries your system and automatically opens a styled HTML dashboard in your browser.
- Hands-on experience with the PowerShell object pipeline pattern: query, filter, select, and format live Windows data using the same approach sysadmins rely on daily.
- A professional HTML health report with colored tables and inline CSS that works as a standalone file you can email, bookmark, or demo.
- Secret Mission: Schedule the script to run daily with Task Scheduler so a fresh report generates every morning without you lifting a finger.
Are there any prerequisites?
You need a Windows machine with VS Code and PowerShell 7 already installed. This guide walks you through everything else, including the VS Code extension and execution policy setup.
Before We Start
Before We Start...
Before we dive in, let's lock in what you're about to build and why it matters.
Set Up Your PowerShell Environment
PowerShell blocks script files from running by default on Windows. Before you can write and execute your health monitor script, you need to change this setting.
You also need the PowerShell extension in VS Code to get syntax highlighting, IntelliSense, and an integrated terminal that speaks PowerShell. In this step, you will install the extension, confirm your terminal is running PowerShell 7, and set the execution policy.
In this step, get ready to:
- Install the PowerShell extension in VS Code.
- Confirm your terminal is running PowerShell 7.
- Configure the execution policy to allow scripts.
Install the PowerShell extension
- Open the Extensions panel by pressing Ctrl+Shift+X.
- Type PowerShell in the search bar at the top of the panel.
- Find the result published by Microsoft with the ID ms-vscode.powershell.
- Click Install.
After installation, look at the bottom-right corner of the VS Code status bar. You should see a {} icon. Hover over it to confirm it shows a PowerShell version number.
What does this extension give you?
The PowerShell extension adds syntax highlighting for .ps1 files, IntelliSense (auto-complete suggestions), and a dedicated PowerShell terminal inside VS Code. Without it, VS Code treats your script as plain text with no help.
Don't see the {} icon in the status bar?
Try closing and reopening VS Code. The extension sometimes needs a restart to activate. If you still do not see it, open the Extensions panel again and confirm the extension status says Enabled.
help me troubleshoot the PowerShell extension not showing in VS Code
Confirm PowerShell 7 in the terminal
Your integrated terminal needs to be running PowerShell 7 (not the older Windows PowerShell 5.1 that ships with Windows). The commands you will write later depend on PowerShell 7 features.
- Switch to your integrated terminal by pressing Ctrl+`.
- Look at the dropdown arrow next to the + icon in the top-right of the terminal panel. If it does not already say pwsh or PowerShell, click the dropdown and select PowerShell.
- Verify your version by running this command:
$PSVersionTable.PSVersion
You should see a version starting with 7 (for example, 7.6.3). This confirms your terminal is using PowerShell 7.
Seeing version 5.1 instead of 7?
Your terminal defaulted to Windows PowerShell 5.1 instead of PowerShell 7. Click the dropdown arrow next to the + icon in the terminal panel and select PowerShell (not "Windows PowerShell"). Then run $PSVersionTable.PSVersion again.
If PowerShell 7 does not appear in the dropdown, you may need to install it. Run this in any terminal:
winget install --id Microsoft.PowerShell --source winget
After installation completes, restart VS Code and PowerShell 7 will appear in the terminal dropdown.
help me get PowerShell 7 working in VS Code
Set the execution policy
Windows uses an execution policy to control which scripts are allowed to run. By default, it may block your .ps1 files. You need to set it to RemoteSigned, which allows local scripts to run while still blocking untrusted scripts downloaded from the internet.
- Set the execution policy by running this command:
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
- When prompted with a confirmation, type Y and press Enter.
Why RemoteSigned and not Unrestricted?
RemoteSigned is the recommended setting for development. It lets you run scripts you write locally, but blocks scripts downloaded from the internet unless they are signed by a trusted publisher. This protects you from accidentally running malicious downloaded scripts.
The -Scope CurrentUser flag means this change only affects your account. It does not require administrator elevation and does not impact other users on the machine.
Now verify the policy is set. Before you run the next command, what do you expect it to print back?
- Check your execution policy by running:
Get-ExecutionPolicy -Scope CurrentUser
You should see RemoteSigned printed back. This confirms Windows will now allow your scripts to run.
Seeing Restricted or an error?
If you see Restricted instead of RemoteSigned, the Set-ExecutionPolicy command did not take effect. Make sure you typed Y at the confirmation prompt. Run the Set-ExecutionPolicy command again and confirm with Y.
If you see a permissions error, make sure you included -Scope CurrentUser in the command. Without it, PowerShell tries to change the machine-wide policy, which requires administrator privileges.
help me fix my PowerShell execution policy
Your environment is ready. You have VS Code with the PowerShell extension, a terminal running PowerShell 7, and permission to execute scripts. Next up, you will run live commands that query real data from your machine.
Query Your System Live
Your environment is ready to execute scripts. Now it is time to see what PowerShell can actually do with live data from your own machine.
In this step, you will run commands interactively and watch real objects come back with named properties you can filter, sort, and reshape. Every command in PowerShell returns structured objects, not plain text. This is the core difference that makes it a serious automation tool.
In this step, get ready to:
- Query your machine's OS version, running processes, and services using PowerShell cmdlets.
- Transform raw output into clean tables using calculated properties and the object pipeline.
Query your operating system info
The first thing a sysadmin checks on any machine is what OS is running and when it last rebooted. CIM (Common Information Model) is the standard interface for querying hardware and OS data on Windows.
- Query your machine's operating system details by running this command in your integrated terminal:
Get-CimInstance -ClassName Win32_OperatingSystem | Select-Object CSName, Caption, Version, LastBootUpTime
You should see a single row with your hostname, Windows edition, version number, and the exact date and time you last rebooted.
What does this command do?
- Get-CimInstance -ClassName Win32_OperatingSystem queries the Windows OS class from the CIM repository. It returns an object with dozens of properties.
- Select-Object CSName, Caption, Version, LastBootUpTime picks just the four properties you care about. Without it, you would see a wall of data.
- The pipe | sends the output of one command into the next. This is the object pipeline pattern you will use throughout PowerShell.
Seeing an error instead of data?
- If you see a red error mentioning "Invalid class", double-check the spelling of Win32_OperatingSystem exactly as shown. CIM class names are specific and a single typo triggers the error.
- Make sure you are running the command in the PowerShell terminal, not Command Prompt. Check the terminal dropdown shows "PowerShell" as the active shell.
Help me troubleshoot my Get-CimInstance command. I'm getting an error when querying Win32_OperatingSystem.
Find the top 5 CPU-hungry processes
Knowing which processes consume the most CPU is essential for troubleshooting a slow machine. This command chains three pipeline stages together.
- Get the top 5 processes by CPU usage by running this command:
Get-Process | Sort-Object CPU -Descending | Select-Object -First 5 Name, Id, CPU, @{Name="MemMB";Expression={[math]::Round($_.WorkingSet/1MB,2)}}
You should see a table with five rows. Each row shows a process name, its ID, cumulative CPU time, and memory usage in megabytes.
What does this command do?
- Get-Process retrieves every running process as an object with properties like Name, CPU, and WorkingSet (memory in bytes).
- Sort-Object CPU -Descending orders the process objects from highest CPU to lowest.
- Select-Object -First 5 keeps only the top five.
- The @{Name="MemMB";Expression={...}} block is a calculated property. It takes the raw WorkingSet value (in bytes) and converts it to megabytes with two decimal places. This is how you reshape data on the fly in PowerShell.
Each row in your output is an object with named properties. This is what makes PowerShell different from batch scripts that only output raw text strings.
Find stopped services that should be running
Windows services run in the background to keep things working. Some are set to start automatically but might have stopped unexpectedly. This query catches those.
- Find services that are stopped but configured for automatic start by running this command:
Get-Service | Where-Object {$_.Status -eq "Stopped" -and $_.StartType -eq "Automatic"} | Select-Object Name, DisplayName, Status
What does this command do?
- Get-Service retrieves every Windows service as an object.
- Where-Object filters down to only services where Status is Stopped AND StartType is Automatic. The $_ symbol represents the current object in the pipeline.
- Select-Object Name, DisplayName, Status picks the three properties that identify each service at a glance.
✔️ I see a table of stopped services
Those are services your machine expects to have running but they have stopped. This is exactly the kind of finding a sysadmin reports on.
ⓧ I see no output at all
An empty result means every automatic service on your machine is currently running. That is a healthy sign. The command worked correctly. You will still see this section in your final report.
Verify with a disk space query
You have queried OS info, processes, and services. Each command followed the same pattern: get data, pipe it, transform it. Now put it all together with a disk space query that uses everything you have learned.
Before you run the next command, predict what columns you expect to see in the output. What data would a sysadmin want about disk space?
- Query your disk space with calculated properties by running this command:
Get-CimInstance -ClassName Win32_LogicalDisk -Filter "DriveType=3" | Select-Object DeviceID, @{Name="SizeGB";Expression={[math]::Round($_.Size/1GB,2)}}, @{Name="FreeGB";Expression={[math]::Round($_.FreeSpace/1GB,2)}}
You should see a table with your actual drive letters (like C:), total size in GB, and free space in GB. Real numbers from your machine, formatted for humans.
What does this command do?
- Win32_LogicalDisk is the CIM class that represents disk drives.
- -Filter "DriveType=3" narrows results to fixed hard disks only. This excludes CD-ROMs, USB drives, and network drives.
- The two calculated properties convert raw byte counts (Size and FreeSpace) into human-readable gigabytes. Without this, you would see numbers like 511101108224 instead of 476.0.
Not seeing any disk data?
- Confirm the filter uses a regular double-quote character, not a curly/smart quote. Copy the command directly from the code block above.
- If you see an error mentioning "Invalid class", check that you typed Win32_LogicalDisk with the exact capitalization and no extra spaces.
Help me troubleshoot my disk space query. I'm not getting the expected output from Get-CimInstance Win32_LogicalDisk.
You have just pulled live system data, filtered it, and reshaped it into clean tables. Next up, you will combine these commands into a reusable script file that runs them all in one go.
Combine Into a Script
In the previous step, you ran PowerShell commands one at a time and saw real data from your machine.
Running commands interactively is great for exploration, but a sysadmin needs reusable tools they can run on demand. In this step, you will save those commands into a .ps1 script file and run it as a single unit. The output prints to the console, which works for quick checks but is not something you can email or show in an interview.
In this step, get ready to:
- Create a PowerShell script file that combines all four health queries.
- Structure the script with variables, section headers, and formatted output.
- Run the script and verify all sections print to the console.
Create the script file and add system info sections
A script file is just a text file with a .ps1 extension. PowerShell reads it top to bottom, exactly like typing each line into the terminal yourself.
- In VS Code, with your C:\Scripts folder open, click File in the top menu bar.
- Click New File.
- Type SystemHealthReport.ps1 as the filename and press Enter.
- Paste the following code into the new file:
# System Health Report Generator
$timestamp = Get-Date
Write-Host "System Health Report - $timestamp" -ForegroundColor Yellow
Write-Host "=== OS Information ===" -ForegroundColor Cyan
$osInfo = Get-CimInstance -ClassName Win32_OperatingSystem |
Select-Object CSName, Caption, Version, LastBootUpTime
$osInfo | Format-Table -AutoSize
Write-Host "=== Disk Space ===" -ForegroundColor Cyan
$diskInfo = Get-CimInstance -ClassName Win32_LogicalDisk -Filter "DriveType=3" |
Select-Object DeviceID,
@{Name="SizeGB";Expression={[math]::Round($_.Size/1GB,2)}},
@{Name="FreeGB";Expression={[math]::Round($_.FreeSpace/1GB,2)}},
@{Name="PercentFree";Expression={[math]::Round(($_.FreeSpace/$_.Size)*100,1)}}
$diskInfo | Format-Table -AutoSize
What does this code do?
This is the first half of your script. Here is what each section handles:
- $timestamp = Get-Date captures the current date and time so you know exactly when the report ran.
- Write-Host with -ForegroundColor Cyan prints colored section headers so each block of data is easy to spot in the terminal.
- $osInfo stores the OS query result in a variable, then pipes it to Format-Table -AutoSize for clean aligned columns.
- $diskInfo queries fixed hard disks (DriveType=3) and uses calculated properties to convert raw bytes into human-readable GB values.
- Save SystemHealthReport.ps1 with Ctrl+S.
- Run the partial script to confirm it works so far by entering this in the integrated terminal:
.\SystemHealthReport.ps1
You should see a yellow timestamp line, then an OS info table with your hostname and Windows version, followed by a disk space table showing your drive letters with sizes in GB.
Seeing a red error instead of output?
- If you see "cannot be loaded because running scripts is disabled", your execution policy was not set correctly in Step 1. Run Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser and try again.
- If you see "cannot find path", make sure your terminal is in C:\Scripts. Run cd C:\Scripts first.
- If you see a syntax error, check that the pipe characters (|) are at the end of lines, not the beginning.
Why is my script not running in PowerShell?
Add process and service monitoring
The second half of the script checks which processes are consuming the most CPU and which critical services have stopped unexpectedly.
- In SystemHealthReport.ps1, place your cursor at the end of the last line ($diskInfo | Format-Table -AutoSize) and press Enter twice to create a blank line.
- Add the following code below the disk space section:
Write-Host "=== Top 5 CPU Processes ===" -ForegroundColor Cyan
$topProcesses = Get-Process |
Sort-Object CPU -Descending |
Select-Object -First 5 Name, Id, CPU,
@{Name="MemMB";Expression={[math]::Round($_.WorkingSet/1MB,2)}}
$topProcesses | Format-Table -AutoSize
Write-Host "=== Stopped Automatic Services ===" -ForegroundColor Cyan
$stoppedServices = Get-Service |
Where-Object {$_.Status -eq "Stopped" -and $_.StartType -eq "Automatic"} |
Select-Object Name, DisplayName, Status
$stoppedServices | Format-Table -AutoSize
What does this code do?
- $topProcesses sorts all running processes by CPU time descending and takes only the top 5. The calculated property converts memory from bytes to megabytes for readability.
- $stoppedServices filters for services that are set to start automatically but are currently stopped. These are services your machine expects to be running but are not.
- Each variable pipes to Format-Table -AutoSize to produce neatly aligned columns in the terminal output.
- Save SystemHealthReport.ps1 with Ctrl+S.
Before you run the full script, what do you expect to see in the terminal output this time? How many sections should appear?
- Run the complete script by entering this in the terminal:
.\SystemHealthReport.ps1
You should see four labeled sections printed to the console: a yellow timestamp header, then OS information, disk space, top 5 CPU processes, and stopped automatic services, each with a cyan header and a formatted table beneath it.
What if the stopped services section is empty?
If no rows appear under the stopped services header, that means your system is healthy. All services that are configured to start automatically are currently running. That is a good sign.
Missing a section or seeing errors?
- If you only see two sections, the code you added for processes and services may not have been saved. Press Ctrl+S and run again.
- If a table prints but looks misaligned, make sure each Select-Object line is indented with spaces, not broken mid-property.
- If you see "The term 'Get-Process' is not recognized", confirm your terminal is running PowerShell (not Command Prompt). Look for PS C:\Scripts> at the start of your prompt.
Help me debug my SystemHealthReport.ps1 script output.
✔️ Awesome, I've got everything!
Great. Double check you have saved SystemHealthReport.ps1 and that all four sections appear when you run it.
ⓧ I'd like to double check the full code
Here is the complete SystemHealthReport.ps1 file. Make sure yours matches exactly:
# System Health Report Generator
$timestamp = Get-Date
Write-Host "System Health Report - $timestamp" -ForegroundColor Yellow
Write-Host "=== OS Information ===" -ForegroundColor Cyan
$osInfo = Get-CimInstance -ClassName Win32_OperatingSystem |
Select-Object CSName, Caption, Version, LastBootUpTime
$osInfo | Format-Table -AutoSize
Write-Host "=== Disk Space ===" -ForegroundColor Cyan
$diskInfo = Get-CimInstance -ClassName Win32_LogicalDisk -Filter "DriveType=3" |
Select-Object DeviceID,
@{Name="SizeGB";Expression={[math]::Round($_.Size/1GB,2)}},
@{Name="FreeGB";Expression={[math]::Round($_.FreeSpace/1GB,2)}},
@{Name="PercentFree";Expression={[math]::Round(($_.FreeSpace/$_.Size)*100,1)}}
$diskInfo | Format-Table -AutoSize
Write-Host "=== Top 5 CPU Processes ===" -ForegroundColor Cyan
$topProcesses = Get-Process |
Sort-Object CPU -Descending |
Select-Object -First 5 Name, Id, CPU,
@{Name="MemMB";Expression={[math]::Round($_.WorkingSet/1MB,2)}}
$topProcesses | Format-Table -AutoSize
Write-Host "=== Stopped Automatic Services ===" -ForegroundColor Cyan
$stoppedServices = Get-Service |
Where-Object {$_.Status -eq "Stopped" -and $_.StartType -eq "Automatic"} |
Select-Object Name, DisplayName, Status
$stoppedServices | Format-Table -AutoSize
Your script works and prints real system data on demand. Next up, you will transform this plain console output into a professional HTML report that opens in your browser.
Generate a Styled HTML Report
Your script works. It queries real system data and prints it to the console. But console output dies the moment you close the terminal.
You cannot bookmark it, email it to a team lead, or open it on another machine. In this step, you will transform those PowerShell objects into a styled HTML report that opens in your browser. This is the difference between "I ran some commands" and "I built a reporting tool."
In this step, get ready to:
- Replace console formatting with HTML fragment generation.
- Add inline CSS for a professional table-based look.
- Assemble and open a styled HTML report in your browser.
Replace Format-Table with HTML fragments
ConvertTo-Html with the -Fragment parameter converts PowerShell objects into an HTML table string. Instead of printing data to the console and losing it, you store each section as an HTML snippet you can combine later into a full page.
- In VS Code, select all the content in SystemHealthReport.ps1 (Ctrl+A) and delete it.
- Replace it with this restructured data collection section:
# System Health Report Generator
# Generates a styled HTML report of system health and opens it in the browser
# --- Configuration ---
$ReportPath = "C:\Scripts\SystemHealth-Report.html"
# --- Data Collection ---
$osInfo = Get-CimInstance -ClassName Win32_OperatingSystem |
Select-Object CSName, Caption, Version, LastBootUpTime
$diskInfo = Get-CimInstance -ClassName Win32_LogicalDisk -Filter "DriveType=3" |
Select-Object DeviceID,
@{Name="SizeGB";Expression={[math]::Round($_.Size/1GB,2)}},
@{Name="FreeGB";Expression={[math]::Round($_.FreeSpace/1GB,2)}},
@{Name="PercentFree";Expression={[math]::Round(($_.FreeSpace/$_.Size)*100,1)}}
$topProcesses = Get-Process |
Sort-Object CPU -Descending |
Select-Object -First 5 Name, Id, CPU,
@{Name="MemMB";Expression={[math]::Round($_.WorkingSet/1MB,2)}}
$stoppedServices = Get-Service |
Where-Object {$_.Status -eq "Stopped" -and $_.StartType -eq "Automatic"} |
Select-Object Name, DisplayName, Status
What changed from the previous version?
- All Write-Host header lines are gone. The HTML report has its own headings, so you no longer need console headers.
- All Format-Table -AutoSize pipes are gone. Format-Table converts objects into text strings, which destroys the structured data. Without it, the variables hold rich objects that ConvertTo-Html can turn into proper table rows.
- $ReportPath stores the output file location so you only define it once.
- Save SystemHealthReport.ps1.
- Verify the data collection still works by dot-sourcing the script, then inspecting a variable:
. .\SystemHealthReport.ps1
$diskInfo
You should see a table of your drives with DeviceID, SizeGB, FreeGB, and PercentFree columns. The data is the same as before, just stored as objects instead of printed text.
Seeing an error about execution policy?
If you see a "cannot be loaded because running scripts is disabled" message, your execution policy reset. Run Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser again and confirm with Y.
Help me fix this execution policy error
Add CSS styling and assemble the HTML page
HTML tables with no styling look like a spreadsheet from 1998. An inline CSS block in the page header gives you professional formatting without any external files. The report stays self-contained as a single file.
- In SystemHealthReport.ps1, add the HTML generation section at the bottom of your script, below the $stoppedServices variable:
# --- HTML Generation ---
$cssStyle = @"
<style>
body { font-family: Segoe UI, Arial, sans-serif; margin: 20px; background: #f5f5f5; }
h1 { color: #2c3e50; border-bottom: 2px solid #3498db; padding-bottom: 10px; }
h2 { color: #2980b9; margin-top: 30px; }
table { border-collapse: collapse; width: 100%; margin-bottom: 20px; background: white; }
th { background-color: #3498db; color: white; padding: 10px; text-align: left; }
td { padding: 8px 10px; border-bottom: 1px solid #ddd; }
tr:hover { background-color: #f0f8ff; }
.footer { color: #7f8c8d; font-size: 0.9em; margin-top: 30px; }
</style>
"@
What is the @" ... "@ syntax?
This is a PowerShell here-string. It lets you define multi-line text (including quotes and special characters) without escaping anything. The opening @" must be the last thing on its line, and the closing "@ must be the first thing on its line.
The CSS styles tables with blue headers, hover effects, and clean spacing. Because the style lives inside the HTML file, the report works anywhere without needing a linked stylesheet.
- Now add the fragment conversion lines directly below the CSS block:
$osHtml = $osInfo | ConvertTo-Html -Fragment -PreContent "<h2>Operating System</h2>"
$diskHtml = $diskInfo | ConvertTo-Html -Fragment -PreContent "<h2>Disk Space</h2>"
$processHtml = $topProcesses | ConvertTo-Html -Fragment -PreContent "<h2>Top 5 CPU Processes</h2>"
$serviceHtml = $stoppedServices | ConvertTo-Html -Fragment -PreContent "<h2>Stopped Automatic Services</h2>"
What does ConvertTo-Html -Fragment do?
The -Fragment parameter generates only an HTML table. It omits the html, head, and body tags. This lets you compose multiple fragments into a single page later.
-PreContent adds text before the opening table tag. Here, it inserts an <h2> heading above each table so the report has clear sections.
- Save the file.
- Dot-source again and check one of the HTML fragments:
. .\SystemHealthReport.ps1
$osHtml
You should see raw HTML in the console, starting with <h2>Operating System</h2> followed by <table> tags. That is your data transformed into HTML, ready to be assembled into a page.
Seeing empty output for $osHtml?
Make sure you added the fragment lines below the data collection section, not above it. The variables $osInfo, $diskInfo, etc. must be populated before you pipe them to ConvertTo-Html.
Help me debug my empty HTML fragment
- Now add the page assembly section at the very bottom of your script, below the four fragment lines:
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$htmlReport = ConvertTo-Html -Head $cssStyle -Title "System Health Report" `
-Body "$osHtml $diskHtml $processHtml $serviceHtml" `
-PostContent "<p class='footer'>Report generated: $timestamp</p>"
$htmlReport | Out-File -FilePath $ReportPath -Encoding utf8
# --- Open in Browser ---
Invoke-Item $ReportPath
What does this assembly code do?
- ConvertTo-Html -Head injects your CSS into the page's <head> tag.
- -Title sets the browser tab name.
- -Body combines all four HTML fragments into the page content.
- -PostContent adds a footer with the generation timestamp after the tables.
- Out-File saves the assembled HTML to C:\Scripts\SystemHealth-Report.html.
- Invoke-Item opens that file in your default browser automatically.
- Save SystemHealthReport.ps1.
Run your report and see it in the browser
Your script is now a complete reporting tool. Time to see if the pipeline from data collection to styled HTML works end to end.
- Before you run this, predict what you think will happen. Will you see console output, a file appear, or something else?
- Run your script:
.\SystemHealthReport.ps1
Your default browser opens automatically with a styled HTML report. You should see blue table headers, your machine's OS info, disk space with GB values, your top 5 CPU processes, and any stopped automatic services.
A footer at the bottom shows the generation timestamp. This is a tool you could demo in an interview or email to a team lead.
Browser did not open?
- Check that Invoke-Item $ReportPath is the very last line of your script. If it is above other code, the file might not exist yet when PowerShell tries to open it.
- Confirm the file was created by running Test-Path C:\Scripts\SystemHealth-Report.html in the terminal. If it returns True, the file exists but your system may not have a default browser associated with .html files. Open the file manually from File Explorer.
- If Test-Path returns False, check that $ReportPath matches "C:\Scripts\SystemHealth-Report.html" exactly (including the backslash and hyphen).
Help me debug why my browser did not open
✔️ Awesome, I've got everything!
Great. Double check you have saved SystemHealthReport.ps1 and that your browser displayed the styled report.
ⓧ I'd like to double check the full code
Your SystemHealthReport.ps1 should have these sections in order. Scroll up and confirm each one is present:
- Header comments and $ReportPath configuration (the "Configuration" section).
- Four data collection variables: $osInfo, $diskInfo, $topProcesses, $stoppedServices (the "Data Collection" section).
- The $cssStyle here-string with inline CSS (the "HTML Generation" comment marks its start).
- Four fragment lines: $osHtml, $diskHtml, $processHtml, $serviceHtml.
- The $timestamp variable, $htmlReport assembly with ConvertTo-Html, and Out-File pipe.
- Invoke-Item $ReportPath as the very last line (the "Open in Browser" section).
You now have a reporting tool, not just a script. Run one command, get a professional HTML dashboard in your browser. This is something you could demo in an interview or email to a team lead.
Secret mission
Schedule It with Task Scheduler
Your script works great when you remember to run it. But what if it ran itself every morning at 7 AM, generating a fresh report before you even sit down? That is real automation. In this secret mission, you will schedule your health monitor with Windows Task Scheduler so it produces daily reports without you lifting a finger.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally with no cloud services, so there are no ongoing costs.
Resources you used:
- C:\Scripts folder containing SystemHealthReport.ps1 and generated HTML report files.
- Task Scheduler task DailyHealthReport (if you completed the Secret Mission).
- PowerShell execution policy changed to RemoteSigned for your user account.
- VS Code PowerShell extension (ms-vscode.powershell).
Keep everything running
No action needed. Choose this if you are still actively building or plan to expand the script with more health checks.
- Your C:\Scripts folder stays in place with your script and reports.
- The DailyHealthReport scheduled task continues generating a fresh HTML report every morning at 7:00 AM.
- Your execution policy remains RemoteSigned, which is the recommended setting for running local scripts safely.
- The PowerShell extension stays installed in VS Code for future scripting projects.
Pause - I'll come back to this later
Disable the scheduled task so it stops running daily, but keep all files intact for when you return.
- Disable the scheduled task by running this command in your PowerShell terminal:
Disable-ScheduledTask -TaskName "DailyHealthReport"
- This stops the task from triggering at 7:00 AM each day, but keeps the task definition registered so you can re-enable it later.
- To re-enable when you come back, run:
Enable-ScheduledTask -TaskName "DailyHealthReport"
- Your C:\Scripts folder and all HTML reports remain on disk.
- Your execution policy and VS Code extension stay as they are.
Delete - I don't want to use this again
Remove all project resources and start fresh. Follow each step below to fully clean up.
- Remove the scheduled task by running this command:
Unregister-ScheduledTask -TaskName "DailyHealthReport" -Confirm:$false
- Delete the entire C:\Scripts folder (script and all generated HTML reports) by running:
Remove-Item -Path "C:\Scripts" -Recurse -Force
- Reset your execution policy back to the Windows default by running:
Set-ExecutionPolicy -ExecutionPolicy Undefined -Scope CurrentUser
- This removes your user-level policy override, reverting the effective policy to the system default (Restricted).
- Uninstall the PowerShell extension in VS Code by opening Extensions (Ctrl+Shift+X), searching for PowerShell, and clicking Uninstall.
Nice Work!
Nice Work!
You just built a production-ready system health monitoring tool from scratch using PowerShell. This is the exact kind of automation that sysadmins build in their first week on the job.
You've learned how to:
- Query live Windows system data using Get-CimInstance, Get-Process, and Get-Service to pull disk space, CPU usage, and service health in real time.
- Transform raw PowerShell objects into a professional styled HTML report using ConvertTo-Html and inline CSS, producing a single self-contained file you can share or demo.
- Master the object pipeline pattern (query | filter | select | format) that underpins all PowerShell automation.
- Secret Mission: Schedule unattended daily execution using Register-ScheduledTask, proving you can automate monitoring without manual intervention.
Ready to quiz yourself?