Building Your First PowerShell Module for Microsoft 365

Stop copy-pasting the same commands every week. Build a module once, use it forever — from any PowerShell window, on any machine.

🎯 Written for IT admins who are new to PowerShell modules
PowerShell M365 Exchange Online Beginner Friendly

If you manage Microsoft 365, you have probably written the same PowerShell commands dozens of times. Connect to Exchange Online. Run Get-Mailbox. Adjust a setting. Disconnect. Next week, same thing — you open your notes, find the script, copy it into a window, and run it again.

There is a better way. It is called a PowerShell module, and once you build one, your commands are available instantly in any PowerShell window — no searching for old scripts, no copy-pasting, no version confusion. You type the name of your function and it works.

This article shows you exactly how to build one from scratch, step by step. By the end, you will have a working module with a real function that sets mailbox quotas in Exchange Online — and you will understand why every IT admin should build one.

What You Need Before Starting

PowerShell 5.1 — already installed on every modern Windows machine. Open the Start menu and search "Windows PowerShell".

ExchangeOnlineManagement module — install it once by running: Install-Module ExchangeOnlineManagement -Scope CurrentUser

M365 admin credentials — you will need an account with Exchange admin rights to test the mailbox quota function.

The Problem With Raw Scripts

Before building something better, it helps to understand exactly what goes wrong with the way most admins work today. Here is a typical Monday morning scenario.

You need to update the mailbox quota for a user. You search your desktop for the script you wrote six months ago. You find three files named mailbox-quota.ps1, mailbox-quota-v2.ps1, and mailbox-quota-FINAL.ps1. You open the newest one, but it references a variable that no longer exists. You fix it, run it, it works. A colleague asks you to share it. You email them the file. They modify their copy. Now there are two different versions doing slightly different things — and neither of you knows which is correct.

✗  Working with raw scripts
📁Scripts scattered across desktops, OneDrive, email
🔄Copy the script file every time you need it
⚠️Multiple versions — nobody knows which is current
🔍No way to see what commands are available
😓Colleagues ask "do you have a script for X?" constantly
✓  Working with a module
📦One folder in a known location on every machine
Type the function name — it just works
One version, always current, everyone uses the same
🔎Get-Command -Module MyModule lists everything
🤝Copy the module folder to share everything at once

What Is a PowerShell Module?

A PowerShell module is a folder with your functions inside it, placed in a location that PowerShell knows to look. That is genuinely all it is at its core.

When PowerShell starts, it scans a set of known folder paths. If it finds a module folder there, it makes all the functions inside that module available automatically — no dot-sourcing, no copying scripts, no file paths. You just type the function name.

Think of it like installing an app on your phone. Once it is installed, you tap the icon and it works — you do not think about where the app files are stored. A module works the same way. Install it once, use it forever.

Where Does a Module Live?

PowerShell looks for modules in a specific list of folders. You can see this list at any time by running:

POWERSHELL
$env:PSModulePath -split ';'

That command shows every folder PowerShell checks. There are three locations you need to know about:

LocationPathWho Can Use It
Current User
Recommended
C:\Users\YourName\Documents\WindowsPowerShell\Modules\ Only you, on this machine. No admin rights needed to install here.
All Users
System-wide
C:\Program Files\WindowsPowerShell\Modules\ Everyone on this machine. Requires admin rights to install here.
Built-in
Read Only
C:\Windows\System32\WindowsPowerShell\v1.0\Modules\ Windows built-in modules. Never put your module here.

Where to Put Your Module

Use the Current User path for personal use — no admin rights required and it will not affect other users on the machine. Use the All Users path when you want to share the module with all admins on a shared jump server.

Why Build a Module? The Advantages

Always Available
Once installed, your functions work from any PowerShell window — no importing scripts, no file paths, no dot-sourcing.
🔍
Discoverable
Run Get-Command -Module MyModule to see every function in your module. Tab completion works too.
📦
One Version for Everyone
Copy the module folder to a new machine or a colleague's jump server. Everyone uses the same code, always up to date.
🧱
Buildable Over Time
Start with one function. Add another next month. Your module grows with your needs — no restructuring required.
🛡️
Safer Operations
Module functions can include built-in safety patterns like -WhatIf preview mode so you see what will change before it happens.
📖
Built-in Help
Add comment-based help to your functions and Get-Help Set-MyMailboxQuota works just like native PowerShell help.

Building Your First Module — Step by Step

You are going to build a real module called MyModule with one practical function: Set-MyMailboxQuota. It connects to Exchange Online, finds the mailbox you specify, shows you a preview of the changes, and only applies them when you confirm.

Follow each step in order. This takes about 10 minutes.

1
Create the module folder
The folder name must match the module name exactly. Run this in PowerShell to create it in the right location:
POWERSHELL
# Create the module folder in the current user module path
$modulePath = "$HOME\Documents\WindowsPowerShell\Modules\MyModule"
New-Item -Path $modulePath -ItemType Directory -Force

# Confirm it was created
Write-Host "Module folder created at: $modulePath" -ForegroundColor Green
2
Create the module file
The module file (.psm1) is where your functions live. The filename must match the folder name. Run this to create it:
POWERSHELL
$moduleFile = "$HOME\Documents\WindowsPowerShell\Modules\MyModule\MyModule.psm1"
New-Item -Path $moduleFile -ItemType File -Force
Write-Host "Module file created: $moduleFile" -ForegroundColor Green

Your module folder now looks like this:

FOLDER STRUCTURE
MyModule/
└── MyModule.psm1       # Your functions go in here
3
Add your first function
Open MyModule.psm1 in Notepad or VS Code and paste the complete function below. Read through the comments — every section is explained.
POWERSHELL MyModule.psm1
function Set-MyMailboxQuota {
    <#
    .SYNOPSIS
        Sets the mailbox storage quota for an Exchange Online mailbox.

    .DESCRIPTION
        Connects to Exchange Online if not already connected, then sets
        the Warning, ProhibitSend, and ProhibitSendReceive quotas based
        on the GB value you provide. Always shows a preview before applying.

    .PARAMETER Identity
        The mailbox to update. Accepts email address or display name.

    .PARAMETER QuotaGB
        The ProhibitSend quota in GB. Warning is set 1 GB below this,
        and hard limit (ProhibitSendReceive) 1 GB above.

    .EXAMPLE
        Set-MyMailboxQuota -Identity "john.doe@company.com" -QuotaGB 50

    .EXAMPLE
        Set-MyMailboxQuota -Identity "john.doe@company.com" -QuotaGB 50 -WhatIf
    #>

    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Identity,

        [Parameter(Mandatory = $true)]
        [int]$QuotaGB
    )

    process {

        # ── Step 1: Connect to Exchange Online if not already connected ────────
        $activeSession = Get-ConnectionInformation -ErrorAction SilentlyContinue |
            Where-Object { $_.State -eq 'Connected' }

        if (-not $activeSession) {
            Write-Host "Connecting to Exchange Online..." -ForegroundColor Cyan
            Connect-ExchangeOnline -ShowBanner:$false -ErrorAction Stop
        }

        # ── Step 2: Confirm the mailbox exists before doing anything ──────────
        $mailbox = Get-Mailbox -Identity $Identity -ErrorAction Stop

        # ── Step 3: Calculate all three quota thresholds from the one value ───
        $warningGB         = $QuotaGB - 1   # Warn the user 1 GB before the limit
        $prohibitSendGB    = $QuotaGB         # Stop sending at the limit
        $prohibitReceiveGB = $QuotaGB + 1   # Hard stop 1 GB above

        # ── Step 4: Always show a clear preview before making any changes ─────
        Write-Host ""
        Write-Host "  Mailbox        : $($mailbox.PrimarySmtpAddress)" -ForegroundColor Cyan
        Write-Host "  Warning Quota  : $warningGB GB"                   -ForegroundColor Yellow
        Write-Host "  Prohibit Send  : $prohibitSendGB GB"               -ForegroundColor Yellow
        Write-Host "  Hard Stop      : $prohibitReceiveGB GB"            -ForegroundColor Yellow
        Write-Host ""

        # ── Step 5: Apply changes — only if -WhatIf was NOT specified ─────────
        if ($PSCmdlet.ShouldProcess($mailbox.PrimarySmtpAddress, "Set mailbox quota to $QuotaGB GB")) {
            Set-Mailbox -Identity $Identity `
                -IssueWarningQuota        "$warningGB GB" `
                -ProhibitSendQuota        "$prohibitSendGB GB" `
                -ProhibitSendReceiveQuota  "$prohibitReceiveGB GB" `
                -UseDatabaseQuotaDefaults  $false `
                -ErrorAction Stop

            Write-Host "  Done. Quota updated successfully." -ForegroundColor Green
        }
    }
}
4
Import the module and verify it loaded
Save the file, then run these two commands in a new PowerShell window:
POWERSHELL
# Load the module
Import-Module MyModule

# Confirm your function is available
Get-Command -Module MyModule

You should see your function listed:

OUTPUT
CommandType  Name                  Version  Source
-----------  ----                  -------  ------
Function     Set-MyMailboxQuota    0.0      MyModule
5
Run a preview first — always
Use -WhatIf to see exactly what will change before applying anything. This is the safest way to run any function for the first time:
POWERSHELL
# Preview only — nothing is changed
Set-MyMailboxQuota -Identity "john.doe@company.com" -QuotaGB 50 -WhatIf
OUTPUT — PREVIEW MODE
  Mailbox        : john.doe@company.com
  Warning Quota  : 49 GB
  Prohibit Send  : 50 GB
  Hard Stop      : 51 GB

  What if: Performing the operation "Set mailbox quota to 50 GB"
           on target "john.doe@company.com".
6
Apply the change
When the preview looks correct, run the same command without -WhatIf:
POWERSHELL
# Apply the change for real
Set-MyMailboxQuota -Identity "john.doe@company.com" -QuotaGB 50
OUTPUT — APPLIED
  Mailbox        : john.doe@company.com
  Warning Quota  : 49 GB
  Prohibit Send  : 50 GB
  Hard Stop      : 51 GB

  Done. Quota updated successfully.

Get Help on Any Function

Because the function has a comment-based help block, PowerShell's built-in help system works automatically. Run Get-Help Set-MyMailboxQuota to see the description, parameters, and examples — the same way you would look up any native PowerShell cmdlet.

What You Just Built

Take a moment to appreciate what this module gives you that a raw script does not. Your function checks for an existing connection before connecting — so running it twice does not open two EXO sessions. It validates the mailbox exists before touching anything. It calculates all three quota thresholds from one input value so you cannot accidentally set them in the wrong order. It shows you a preview before making any change. And it is available in any PowerShell window by name, right now, without finding a file.

That is the difference between a script and a module function. The function handles the details so you do not have to think about them every time.

What to Build Next

Your module has one function today. Next time you write a PowerShell command you will use again, add it as a function in MyModule.psm1. Over time your module becomes a personal toolkit — everything you need, available instantly.

Some ideas for your next functions:

  • Get-MyMailboxSize — report the current size and quota of any mailbox
  • Set-MyAutoReply — configure out-of-office replies with HTML body support
  • Get-MyLicenseStatus — check which M365 licences a user has assigned
  • Disable-MyAccount — offboarding function that disables the AD account, revokes sessions, and sets an auto-reply in one call

Taking It Further

Once you are comfortable with the basics, the natural next step is structuring your module for a hybrid Exchange environment — where you need to manage both on-premises Exchange and Exchange Online from the same session. That introduces a dual session guard pattern, forest-wide AD identity resolution, and a folder structure that scales across dozens of functions. Those patterns are covered in the advanced follow-up to this article.

Key Takeaways

  • A PowerShell module is a folder with your functions inside it, placed where PowerShell knows to look. Nothing more complicated than that.
  • The Current User module path — ~\Documents\WindowsPowerShell\Modules\ — requires no admin rights and works immediately.
  • Once installed, your functions are available in any PowerShell window by name. No file paths, no dot-sourcing, no searching.
  • Always include SupportsShouldProcess on any function that changes something, so -WhatIf works and admins can preview before applying.
  • A module grows with you. Start with one function. Add another when you need it. Your toolkit builds itself over time.
  • Get-Command -Module MyModule lists everything in your module. Get-Help FunctionName shows the documentation. These work automatically with no extra setup.