Skip to the main content.

ScriptRunner Blog

Building Your First PowerShell Module

Table of Contents

Post Featured Image

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?

At its most basic, a PowerShell module consists of two required components and one optional. Together these components are the fewest recommended components to use in 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

Crafting a module is not difficult, but structuring the exposed functionality can be. The purpose, of this article, is to create a simple module with three functions. Often modules contain functionality related to a single product or tool. The functions in our module target three different products but support a specific workflow. The workflow is to retrieve SharePoint members, trigger an onboarding Flow, and finally send the SharePoint group members in a Team’s message.
  • New-TeamsMessage
  • Invoke-PowerAutomateFlow
  • Get-SharePointMember

Structure of a Module

The structure of the module itself is pretty simple. The folder and file layout are as described below. One question you may have is why we prepend func_ to the function file. This is a personal preference and not necessary. This technique is useful to avoid dot-sourcing errant scripts in the module’s directories. You will see how to use this technique in the module loading file.


  • func_New-TeamsMessage.ps1
  • func_Invoke-PowerAutomateFlow.ps1
  • func_Get-SharePointMember.ps1

Private – Empty

  • UtilityModule.psd1
  • UtilityModule.psm1

Creating the Module

The fastest way to create a module definition file is the New-ModuleManifest command. Below, we are defining a handful of parameters that will create a usable module. CompatiblePSEditions defines both Desktop and Core as supported PowerShell versions. Command discovery works if you populate the FunctionsToExport parameter.
$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.



	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.



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

After creating the module, we need to define our functions. In this example, we are creating three simple functions that support the intended workflow.


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 { 
				[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" = '' 
									"summary" = $Title 
									"title" = $Title 
									"text" = ($Message | Out-String) 
						"ContentType" = 'application/json' 
				Invoke-RestMethod @Params | Out-Null 



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 { 
				[Parameter(Position = 0, Mandatory = $true)][String]$URI 
				Process { 
					$Params = @{ 
							"URI" = $URI 
							"ContentType" = 'application/json' 
							"Method" = 'GET' 
					Invoke-WebRequest @Params 



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 { 
				[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://...'


Creating a module could not be simpler in PowerShell. To make a system administrator’s life easy, bundle common functions together into a module. There is a lot more functionality available and this tutorial only scratches the surface. Exploring the possibilities that PowerShell modules offer will unlock an easier administrative experience!

Related posts

3 min read

ScriptRunner now available in the Microsoft Azure Marketplace

6 min read

Managing Microsoft Exchange with PowerShell

2 min read

VMUG Webcast: Mastering VMware Management with PowerCLI

About the author: