Building Your First PowerShell Module
Table of Contents

PowerShell has a variety of powerful cmdlets built-in to the core language. But native functions only take you so far, and in most projects you will need more functionality. The PowerShell Gallery offers a wide range of downloadable third-party modules, but there’s also the option to create custom PowerShell modules. This article will guide you through the process of creating your first PowerShell module.
What is a PowerShell Module?
- psd1 File – PowerShell definition file
- psm1 File – PowerShell module loading file
- Functions
There are two types of modules, script modules, and binary modules. In this example, we are building a script module. Consisting of traditional PowerShell functions, a script function is easy to build. A binary module is a .NET framework compiled assembly. Written in .NET, cmdlets are not as approachable as script modules. First, we need to define the purpose of the PowerShell module. Not all aspects of a module need mapping before starting. Creating a clear plan makes defining necessary functions much easier.
Defining the Module’s Purpose
Structure of a Module
Public
- func_New-TeamsMessage.ps1
- func_Invoke-PowerAutomateFlow.ps1
- func_Get-SharePointMember.ps1
Private – Empty
- UtilityModule.psd1
- UtilityModule.psm1
Creating the Module
$Params = @{ "Path" = 'D:\WorkingFolder\Articles\UtilityModule.psd1' "Author" = 'Fake Author' "CompanyName" = 'Fake Company' "RootModule" = 'UtilityModule.psm1' "CompatiblePSEditions" = @('Desktop','Core') "FunctionsToExport" = @('Get-SharePointMember','Invoke-PowerAutomateFlow','New-TeamsMessage') "CmdletsToExport" = @() "VariablesToExport" = '' "AliasesToExport" = @() "Description" = 'Utility Module' } New-ModuleManifest @Params
We export functions using Export-ModuleMember, but performance best practices dictate the use of empty arrays in the module definition. For unknown reasons, an empty array does not export correctly under VariablesToExport, but an empty string does output an empty array.
Running the New-ModuleManifest command creates a UtilityModule.psd1 file. Many comment blocks pertain to extra configurations. Below, we have removed all extra comments to show the configured parameters.
UtilityModule.psd1
@{ RootModule = 'UtilityModule.psm1' ModuleVersion = '0.0.1' CompatiblePSEditions = 'Desktop', 'Core' GUID = 'dc18a919-f4bf-4da2-8c76-24b68fa33ef0' Author = 'Fake Author' CompanyName = 'Fake Company' Copyright = '(c) Fake Author. All rights reserved.' Description = 'UtilityModule' FunctionsToExport = 'Get-SharePointMember','Invoke-PowerAutomateFlow','New-TeamsMessage' CmdletsToExport = @() VariablesToExport = @() AliasesToExport = @() PrivateData = @{ PSData = @{} } }
If all module functions are dot-sourced, then why do we need to list the functions to export? There are two primary reasons for explicitly listing exported functions. Private functions should not be made public, and exported functions will auto-complete on the command line even if the module is not yet loaded.
Next we need to create the module loading file. Again, there are several ways to approach this, but the below method has been reliable. The first section will only retrieve func_ prefixed files and dot-source them. The second will export all functions in the Public folder.
UtilityModule.psm1
Get-ChildItem (Split-Path $script:MyInvocation.MyCommand.Path) -Filter 'func_*.ps1' -Recurse | ForEach-Object { . $_.FullName } Get-ChildItem "$(Split-Path $script:MyInvocation.MyCommand.Path)\Public\*" -Filter 'func_*.ps1' -Recurse | ForEach-Object { Export-ModuleMember -Function ($_.BaseName -Split "_")[1] }
Creating our Functions
New-TeamsMessage
This function will take in a message string and team ID string and create the REST call to create a Teams message. This wraps the necessary JSON formatting into a simplified API call.
Function New-TeamsMessage { [CmdletBinding()] Param( [Parameter(Position = 0, Mandatory = $true)][String]$Message, [Parameter(Position = 1, Mandatory = $true)][String]$Title, [Parameter(Position = 2, Mandatory = $true)][String]$URI ) Process { $Params = @{ "URI" = $URI "Method" = 'POST' "Body" = [PSCustomObject][Ordered]@{ "@type" = 'MessageCard' "@context" = 'http://schema.org/extension' "summary" = $Title "title" = $Title "text" = ($Message | Out-String) } "ContentType" = 'application/json' } Invoke-RestMethod @Params | Out-Null } }
Invoke-PowerAutomateFlow
Next, we are defining a Power Automate Flow function. There is a simple way to do this by using the HTTP trigger, “When a HTTP request is received”. This uses the given URI displayed upon saving to call the trigger.
Function Invoke-PowerAutomateFlow { [CmdletBinding()] Param( [Parameter(Position = 0, Mandatory = $true)][String]$URI ) Process { $Params = @{ "URI" = $URI "ContentType" = 'application/json' "Method" = 'GET' } Invoke-WebRequest @Params } }
Get-SharePointMember
Get-SharePointMember supports our final workflow need of retrieving a SharePoint member list. This function gets all SharePoint site members for a given site and displays the results. Passing in a group will filter the results.
Function Get-SharePointMember { [CmdletBinding()] Param( [Parameter(Position = 0, Mandatory = $true)][String]$URI, [Parameter(Position = 1)][String]$Group ) Process { $Params = @{ "URI" = $URI } If ($Group) { Get-SPOUser @Params | Where-Object -Contains $Group } Else { Get-SPOUser @Params } } }
Importing and Testing the Module
Import the module using the Import-Module command and verify there are no errors. Next, we will use Get-Command to see all exported members.
Import-Module -Name 'UtilityModule' Get-Command -Module 'UtilityModule'
To test, run the various commands and make sure that they are functioning as you would expect. In the script below we are running the functions and utilizing their output.
# Get SharePoint Members $Members = Get-SharePointMember # Invoke Invoke-PowerAutomateFlow -URI 'https://...' New-TeamsMessage -Message $Groups -Title 'Group Members' -URI 'https://...'
Conclusion
Related posts
11 min read
How to connect to Exchange Online with certificate based authentication (CBA)
Jul 25, 2023 by Damian Scoles
About the author:
Adam is a 20+ year veteran of IT and experienced online business professional. He’s an entrepreneur, IT influencer, Microsoft MVP, blogger, trainer, author and content marketing writer for multiple technology companies.
Latest posts:
- Licensing with Microsoft Graph PowerShell
- ScriptRunner Ultimate Edition 6 – AI‑powered scripting
- How to connect to Exchange Online with certificate based authentication (CBA)
- Get-View in PowerCLI – How to manage your VMware infrastructure more efficiently (part 3)
- Automate snapshots and templates with PowerCLI – Part 2