PowerShell’s true power lies in its extensibility. While built-in cmdlets cover many scenarios, custom cmdlets and advanced functions allow you to create reusable, professional-grade tools tailored to your specific needs. This comprehensive guide explores how to build custom cmdlets and advanced functions that integrate seamlessly with PowerShell’s ecosystem.

Understanding the Foundation

Before diving into creation, it’s essential to understand the distinction between functions and cmdlets. Functions are PowerShell script blocks that can accept parameters and return output. Advanced functions extend this concept by adding cmdlet-like behavior through parameter attributes and automatic features.

Creating Custom Cmdlets and Advanced Functions in PowerShell: Complete Developer Guide

Simple Functions vs Advanced Functions

Simple functions provide basic functionality without the overhead of cmdlet features. They’re perfect for straightforward tasks but lack advanced parameter processing and pipeline integration.

# Simple Function
function Get-SimpleGreeting {
    param($Name)
    "Hello, $Name!"
}

Get-SimpleGreeting -Name "Alex"

Output:

Hello, Alex!

Creating Advanced Functions with CmdletBinding

Advanced functions transform ordinary functions into cmdlet-like tools by adding the [CmdletBinding()] attribute. This single addition unlocks automatic features like common parameters, parameter validation, and improved error handling.

function Get-AdvancedGreeting {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string]$Name,
        
        [Parameter(Mandatory=$false)]
        [string]$Title = "Friend"
    )
    
    Write-Verbose "Processing greeting for $Name"
    "Hello, $Title $Name!"
}

# Usage with common parameters
Get-AdvancedGreeting -Name "Alex" -Title "Dr." -Verbose

Output:

VERBOSE: Processing greeting for Alex
Hello, Dr. Alex!

Parameter Attributes and Validation

Parameter attributes provide declarative control over how parameters behave. They enforce validation rules, define positions, and control pipeline input without writing conditional logic.

function New-UserAccount {
    [CmdletBinding(SupportsShouldProcess=$true)]
    param(
        [Parameter(Mandatory=$true, Position=0)]
        [ValidateLength(3,20)]
        [string]$Username,
        
        [Parameter(Mandatory=$true)]
        [ValidateSet("Admin", "User", "Guest")]
        [string]$Role,
        
        [Parameter(Mandatory=$false)]
        [ValidateRange(18,100)]
        [int]$Age,
        
        [Parameter(Mandatory=$false)]
        [ValidatePattern("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$")]
        [string]$Email
    )
    
    if ($PSCmdlet.ShouldProcess($Username, "Create user account")) {
        Write-Output "Creating account for $Username with role: $Role"
        
        if ($Age) {
            Write-Output "Age: $Age"
        }
        
        if ($Email) {
            Write-Output "Email: $Email"
        }
    }
}

# Valid usage
New-UserAccount -Username "jdoe" -Role "User" -Age 25 -Email "[email protected]"

Output:

Creating account for jdoe with role: User
Age: 25
Email: [email protected]

Pipeline Support and ValueFromPipeline

Pipeline integration is a hallmark of PowerShell cmdlets. Advanced functions support pipeline input through the ValueFromPipeline and ValueFromPipelineByPropertyName attributes, combined with Begin, Process, and End blocks.

Creating Custom Cmdlets and Advanced Functions in PowerShell: Complete Developer Guide

function Get-FileSize {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)]
        [Alias("FullName")]
        [string[]]$Path
    )
    
    begin {
        Write-Verbose "Starting file size calculation"
        $totalSize = 0
        $fileCount = 0
    }
    
    process {
        foreach ($file in $Path) {
            if (Test-Path $file -PathType Leaf) {
                $item = Get-Item $file
                $sizeInKB = [math]::Round($item.Length / 1KB, 2)
                
                [PSCustomObject]@{
                    FileName = $item.Name
                    SizeKB = $sizeInKB
                    LastModified = $item.LastWriteTime
                }
                
                $totalSize += $item.Length
                $fileCount++
            }
        }
    }
    
    end {
        Write-Verbose "Processed $fileCount files"
        $totalKB = [math]::Round($totalSize / 1KB, 2)
        Write-Host "`nTotal: $totalKB KB across $fileCount files" -ForegroundColor Green
    }
}

# Pipeline usage
Get-ChildItem "C:\Temp" -File | Get-FileSize -Verbose

Sample Output:

VERBOSE: Starting file size calculation

FileName      SizeKB LastModified       
--------      ------ ------------       
document.txt    15.23 10/20/2025 2:30:00 PM
data.csv        142.87 10/21/2025 9:15:00 AM
notes.md         3.45 10/22/2025 11:00:00 AM

VERBOSE: Processed 3 files
Total: 161.55 KB across 3 files

Parameter Sets for Flexible Interfaces

Parameter sets allow a single function to support multiple usage patterns. They define mutually exclusive parameter combinations, making your cmdlets more versatile while maintaining clarity.

function Connect-ServiceAccount {
    [CmdletBinding(DefaultParameterSetName="Credential")]
    param(
        [Parameter(Mandatory=$true, ParameterSetName="Credential")]
        [PSCredential]$Credential,
        
        [Parameter(Mandatory=$true, ParameterSetName="Token")]
        [string]$AccessToken,
        
        [Parameter(Mandatory=$true, ParameterSetName="Certificate")]
        [string]$CertificateThumbprint,
        
        [Parameter(Mandatory=$true)]
        [string]$ServiceUrl
    )
    
    $authMethod = $PSCmdlet.ParameterSetName
    
    switch ($authMethod) {
        "Credential" {
            Write-Output "Connecting to $ServiceUrl with username: $($Credential.UserName)"
        }
        "Token" {
            Write-Output "Connecting to $ServiceUrl with access token"
        }
        "Certificate" {
            Write-Output "Connecting to $ServiceUrl with certificate: $CertificateThumbprint"
        }
    }
}

# Different authentication methods
Connect-ServiceAccount -ServiceUrl "https://api.example.com" -Credential (Get-Credential)
Connect-ServiceAccount -ServiceUrl "https://api.example.com" -AccessToken "abc123xyz"

Error Handling and Terminating vs Non-Terminating Errors

Professional cmdlets handle errors gracefully. PowerShell distinguishes between terminating errors (which stop execution) and non-terminating errors (which allow continued processing).

Creating Custom Cmdlets and Advanced Functions in PowerShell: Complete Developer Guide

function Test-NetworkConnection {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [string[]]$ComputerName,
        
        [Parameter()]
        [int]$TimeoutMs = 1000
    )
    
    process {
        foreach ($computer in $ComputerName) {
            try {
                Write-Verbose "Testing connection to $computer"
                
                if ([string]::IsNullOrWhiteSpace($computer)) {
                    $errorRecord = [System.Management.Automation.ErrorRecord]::new(
                        [System.ArgumentException]::new("Computer name cannot be empty"),
                        "EmptyComputerName",
                        [System.Management.Automation.ErrorCategory]::InvalidArgument,
                        $computer
                    )
                    $PSCmdlet.WriteError($errorRecord)
                    continue
                }
                
                $ping = Test-Connection -ComputerName $computer -Count 1 -TimeoutSeconds ($TimeoutMs/1000) -ErrorAction Stop
                
                [PSCustomObject]@{
                    ComputerName = $computer
                    Status = "Online"
                    ResponseTime = $ping.ResponseTime
                    IPAddress = $ping.Address
                }
            }
            catch {
                Write-Warning "Failed to connect to $computer: $($_.Exception.Message)"
                
                [PSCustomObject]@{
                    ComputerName = $computer
                    Status = "Offline"
                    ResponseTime = $null
                    IPAddress = $null
                }
            }
        }
    }
}

# Usage
"localhost", "invalid-host", "127.0.0.1" | Test-NetworkConnection -Verbose

Output Types and Custom Objects

Defining output types improves discoverability and enables IntelliSense. Custom objects provide structured, consistent output that integrates seamlessly with PowerShell’s pipeline and formatting system.

function Get-ProcessInfo {
    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory=$false)]
        [string]$Name = "*",
        
        [Parameter()]
        [switch]$IncludeModules
    )
    
    $processes = Get-Process -Name $Name -ErrorAction SilentlyContinue
    
    foreach ($proc in $processes) {
        $output = [PSCustomObject]@{
            PSTypeName = "CodeLucky.ProcessInfo"
            ProcessName = $proc.Name
            ProcessId = $proc.Id
            MemoryMB = [math]::Round($proc.WorkingSet64 / 1MB, 2)
            CPUSeconds = [math]::Round($proc.CPU, 2)
            StartTime = $proc.StartTime
            Responding = $proc.Responding
        }
        
        if ($IncludeModules) {
            $output | Add-Member -NotePropertyName "ModuleCount" -NotePropertyValue $proc.Modules.Count
        }
        
        $output
    }
}

# Usage
Get-ProcessInfo -Name "powershell" -IncludeModules | Format-Table -AutoSize

Sample Output:

ProcessName ProcessId MemoryMB CPUSeconds StartTime              Responding ModuleCount
----------- --------- -------- ---------- ---------              ---------- -----------
powershell       8456   145.23      12.45 10/22/2025 1:30:00 PM       True          89
powershell      12304    98.76       5.32 10/22/2025 2:15:00 PM       True          85

Dynamic Parameters for Context-Sensitive Behavior

Dynamic parameters adapt based on other parameter values or runtime context. They provide sophisticated interfaces that respond to user input, similar to how Get-ChildItem exposes different parameters based on the provider.

function Get-DataSource {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [ValidateSet("SQL", "API", "File")]
        [string]$SourceType
    )
    
    DynamicParam {
        $paramDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
        
        if ($SourceType -eq "SQL") {
            $serverParam = New-Object System.Management.Automation.RuntimeDefinedParameter(
                "ServerName",
                [string],
                (New-Object System.Collections.ObjectModel.Collection[System.Attribute])
            )
            $serverParam.Attributes.Add((New-Object System.Management.Automation.ParameterAttribute -Property @{ Mandatory = $true }))
            $paramDictionary.Add("ServerName", $serverParam)
            
            $databaseParam = New-Object System.Management.Automation.RuntimeDefinedParameter(
                "DatabaseName",
                [string],
                (New-Object System.Collections.ObjectModel.Collection[System.Attribute])
            )
            $databaseParam.Attributes.Add((New-Object System.Management.Automation.ParameterAttribute -Property @{ Mandatory = $true }))
            $paramDictionary.Add("DatabaseName", $databaseParam)
        }
        elseif ($SourceType -eq "API") {
            $urlParam = New-Object System.Management.Automation.RuntimeDefinedParameter(
                "ApiUrl",
                [string],
                (New-Object System.Collections.ObjectModel.Collection[System.Attribute])
            )
            $urlParam.Attributes.Add((New-Object System.Management.Automation.ParameterAttribute -Property @{ Mandatory = $true }))
            $paramDictionary.Add("ApiUrl", $urlParam)
        }
        elseif ($SourceType -eq "File") {
            $pathParam = New-Object System.Management.Automation.RuntimeDefinedParameter(
                "FilePath",
                [string],
                (New-Object System.Collections.ObjectModel.Collection[System.Attribute])
            )
            $pathParam.Attributes.Add((New-Object System.Management.Automation.ParameterAttribute -Property @{ Mandatory = $true }))
            $paramDictionary.Add("FilePath", $pathParam)
        }
        
        return $paramDictionary
    }
    
    process {
        Write-Output "Connecting to $SourceType source"
        
        switch ($SourceType) {
            "SQL" {
                Write-Output "Server: $($PSBoundParameters['ServerName'])"
                Write-Output "Database: $($PSBoundParameters['DatabaseName'])"
            }
            "API" {
                Write-Output "API URL: $($PSBoundParameters['ApiUrl'])"
            }
            "File" {
                Write-Output "File Path: $($PSBoundParameters['FilePath'])"
            }
        }
    }
}

# Usage - different parameters appear based on SourceType
Get-DataSource -SourceType SQL -ServerName "SQLSERVER01" -DatabaseName "Production"
Get-DataSource -SourceType API -ApiUrl "https://api.example.com/v1"

Compiled Cmdlets with C#

For maximum performance and integration with .NET libraries, compiled cmdlets written in C# provide the ultimate extensibility. They require more setup but offer superior performance and full access to the .NET ecosystem.

Creating Custom Cmdlets and Advanced Functions in PowerShell: Complete Developer Guide

// Save as Get-MathOperation.cs
using System;
using System.Management.Automation;

namespace CodeLucky.PowerShell
{
    [Cmdlet(VerbsCommon.Get, "MathOperation")]
    [OutputType(typeof(double))]
    public class GetMathOperationCommand : PSCmdlet
    {
        [Parameter(Mandatory = true, Position = 0)]
        public double FirstNumber { get; set; }

        [Parameter(Mandatory = true, Position = 1)]
        public double SecondNumber { get; set; }

        [Parameter(Mandatory = true)]
        [ValidateSet("Add", "Subtract", "Multiply", "Divide")]
        public string Operation { get; set; }

        protected override void ProcessRecord()
        {
            double result = 0;

            switch (Operation)
            {
                case "Add":
                    result = FirstNumber + SecondNumber;
                    break;
                case "Subtract":
                    result = FirstNumber - SecondNumber;
                    break;
                case "Multiply":
                    result = FirstNumber * SecondNumber;
                    break;
                case "Divide":
                    if (SecondNumber == 0)
                    {
                        ThrowTerminatingError(new ErrorRecord(
                            new DivideByZeroException("Cannot divide by zero"),
                            "DivisionByZero",
                            ErrorCategory.InvalidArgument,
                            SecondNumber));
                        return;
                    }
                    result = FirstNumber / SecondNumber;
                    break;
            }

            WriteObject(result);
        }
    }
}

Compilation and Usage:

# Compile the cmdlet
Add-Type -Path "Get-MathOperation.cs" -ReferencedAssemblies System.Management.Automation

# Use the compiled cmdlet
Get-MathOperation -FirstNumber 10 -SecondNumber 5 -Operation Multiply

Output:

50

Best Practices for Custom Cmdlets

Naming Conventions

Follow PowerShell’s verb-noun naming pattern using approved verbs from Get-Verb. This ensures consistency and predictability across the PowerShell ecosystem.

# Good naming
Get-UserProfile
New-DatabaseConnection
Set-ConfigurationValue
Remove-TemporaryFile

# Poor naming
Fetch-User          # Use Get instead of Fetch
CreateDB            # Use New and full noun
UpdateConfig        # Use Set
DeleteTemp          # Use Remove

Parameter Design

Design parameters with usability in mind. Support pipeline input where logical, provide sensible defaults, and use parameter sets to avoid confusion.

function Export-DataReport {
    [CmdletBinding(DefaultParameterSetName="CSV")]
    param(
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [PSCustomObject[]]$Data,
        
        [Parameter(Mandatory=$true)]
        [string]$OutputPath,
        
        [Parameter(ParameterSetName="CSV")]
        [switch]$AsCSV,
        
        [Parameter(ParameterSetName="JSON")]
        [switch]$AsJSON,
        
        [Parameter(ParameterSetName="XML")]
        [switch]$AsXML,
        
        [Parameter()]
        [switch]$Force
    )
    
    begin {
        $items = @()
    }
    
    process {
        $items += $Data
    }
    
    end {
        if ((Test-Path $OutputPath) -and -not $Force) {
            throw "Output file exists. Use -Force to overwrite."
        }
        
        switch ($PSCmdlet.ParameterSetName) {
            "CSV" { $items | Export-Csv -Path $OutputPath -NoTypeInformation }
            "JSON" { $items | ConvertTo-Json -Depth 10 | Set-Content -Path $OutputPath }
            "XML" { $items | Export-Clixml -Path $OutputPath }
        }
        
        Write-Output "Exported $($items.Count) items to $OutputPath"
    }
}

Documentation with Comment-Based Help

Comprehensive help documentation makes your cmdlets accessible to other users. Comment-based help integrates with Get-Help and provides examples, descriptions, and parameter documentation.

function Get-SystemReport {
    <#
    .SYNOPSIS
        Generates a comprehensive system report.
    
    .DESCRIPTION
        The Get-SystemReport cmdlet collects system information including CPU, memory, 
        disk space, and running services. It supports filtering by report type and 
        exporting results to various formats.
    
    .PARAMETER ReportType
        Specifies the type of report to generate. Valid values are Hardware, Software, 
        Network, and All. Default is All.
    
    .PARAMETER ComputerName
        Specifies the computer name to query. Defaults to the local computer.
    
    .PARAMETER ExportPath
        Optional path to export the report. Supports CSV, JSON, and HTML formats 
        based on file extension.
    
    .EXAMPLE
        Get-SystemReport
        
        Generates a complete system report for the local computer.
    
    .EXAMPLE
        Get-SystemReport -ReportType Hardware -ComputerName SERVER01
        
        Generates a hardware-only report for the remote computer SERVER01.
    
    .EXAMPLE
        Get-SystemReport -ExportPath "C:\Reports\system-report.html"
        
        Generates a report and exports it to an HTML file.
    
    .INPUTS
        String
        You can pipe computer names to Get-SystemReport.
    
    .OUTPUTS
        PSCustomObject
        Returns custom objects containing system information.
    
    .NOTES
        Author: CodeLucky.com
        Version: 1.0.0
        Requires: PowerShell 5.1 or later
    
    .LINK
        https://codelucky.com/powershell/system-reports
    #>
    
    [CmdletBinding()]
    param(
        [Parameter()]
        [ValidateSet("Hardware", "Software", "Network", "All")]
        [string]$ReportType = "All",
        
        [Parameter(ValueFromPipeline=$true)]
        [string]$ComputerName = $env:COMPUTERNAME,
        
        [Parameter()]
        [string]$ExportPath
    )
    
    # Implementation here
    Write-Output "Generating $ReportType report for $ComputerName"
}

# Access help
Get-Help Get-SystemReport -Full
Get-Help Get-SystemReport -Examples

Testing and Debugging Custom Cmdlets

Robust testing ensures your cmdlets behave correctly across various scenarios. Use Pester for unit testing and leverage PowerShell’s debugging capabilities.

# Pester test example (Save as Get-UserProfile.Tests.ps1)
BeforeAll {
    . "$PSScriptRoot\Get-UserProfile.ps1"
}

Describe "Get-UserProfile" {
    Context "Parameter Validation" {
        It "Should require Username parameter" {
            { Get-UserProfile } | Should -Throw
        }
        
        It "Should accept valid username" {
            { Get-UserProfile -Username "testuser" } | Should -Not -Throw
        }
    }
    
    Context "Output Validation" {
        It "Should return PSCustomObject" {
            $result = Get-UserProfile -Username "testuser"
            $result | Should -BeOfType [PSCustomObject]
        }
        
        It "Should include required properties" {
            $result = Get-UserProfile -Username "testuser"
            $result.PSObject.Properties.Name | Should -Contain "Username"
            $result.PSObject.Properties.Name | Should -Contain "ProfilePath"
        }
    }
    
    Context "Pipeline Support" {
        It "Should accept pipeline input" {
            { "user1", "user2" | Get-UserProfile } | Should -Not -Throw
        }
    }
}

# Run tests
Invoke-Pester -Path "Get-UserProfile.Tests.ps1"

Performance Optimization

Optimize your cmdlets for performance by minimizing object creation, using efficient collection types, and avoiding unnecessary iterations.

function Get-LargeDataSet {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [int]$ItemCount
    )
    
    begin {
        # Use ArrayList for better performance with large collections
        $results = [System.Collections.ArrayList]::new($ItemCount)
        $stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
    }
    
    process {
        for ($i = 1; $i -le $ItemCount; $i++) {
            # Avoid += operator with arrays (creates new array each time)
            [void]$results.Add([PSCustomObject]@{
                Id = $i
                Name = "Item$i"
                Value = Get-Random -Minimum 1 -Maximum 1000
            })
            
            # Progress reporting for long operations
            if ($i % 1000 -eq 0) {
                Write-Progress -Activity "Generating data" -Status "$i of $ItemCount" -PercentComplete (($i / $ItemCount) * 100)
            }
        }
    }
    
    end {
        Write-Progress -Activity "Generating data" -Completed
        $stopwatch.Stop()
        Write-Verbose "Generated $ItemCount items in $($stopwatch.ElapsedMilliseconds)ms"
        
        # Return as array for pipeline compatibility
        , $results.ToArray()
    }
}

# Usage
$data = Get-LargeDataSet -ItemCount 10000 -Verbose

Conclusion

Creating custom cmdlets and advanced functions transforms you from a PowerShell user into a PowerShell developer. By mastering parameter binding, pipeline support, error handling, and best practices, you can build professional-grade tools that integrate seamlessly with PowerShell’s ecosystem. Whether you choose advanced functions for flexibility or compiled cmdlets for performance, the principles remain consistent: follow conventions, prioritize usability, and leverage PowerShell’s powerful features.

Start with simple advanced functions, gradually incorporate more sophisticated features like dynamic parameters and custom formatting, and test thoroughly. Your custom cmdlets will become invaluable tools in your automation arsenal, enabling you to tackle complex scenarios with elegant, reusable solutions.