Yeah this ain’t no blog about Harry Potter (don’t even know if the title matches with a corresponding Harry Potter ‘spell’), but we are going to talk about some magic anyway today! 😉
You know that feeling when you’re working in PowerShell, everything’s flowing beautifully with objects, pipelines, variables which make sense and then…. A dark cloud comes in… you need to step to another syntax to use Docker, Azure, Kubernetes, Git or whatever! And you realized that the thing you now need doesn’t have any PowerShell cmdlets available! (This is where the pain strikes in)
Suddenly it feels like you are back in time and you are parsing text output again like we’re in 1995 (Like I know, I was 5 and playing outdoors in that year 😆 but just to make a point).
But, what if I told you that there is a Microsoft-built tool that lets you wrap ANY, yeah you heard me! ANY! Command-line utility into proper PowerShell cmdlets, complete with tab completion, parameter validation, pipeline support and object output, using nothing more than JSON configuration files?
Sound like we’re back in this millennium right?
Well, that’s exactly what we’ll be seeing in today’s blog post, we’ll be talking about Cresendo!
🎬 See this one? Time to stop relaxing and get your hands dirty!
📒 This one? Well, this one is there for highlighting things to you. Whether I believe extra information is required about a section, I’ll post it here.
Enough smooth talk! Let’s get started!
What is Cresendo?
At its core, PowerShell Crescendo is a framework for generating PowerShell modules, but instead of writing pages of PowerShell code by hand, you describe your cmdlet in a simple JSON configuration file. This JSON acts as a blueprint that tells Crescendo how to wrap your external tool: what parameters to expose, what the output should look like, and how to handle things like help documentation or error handling.
Then, Crescendo takes that JSON and generates a PowerShell module for you. The end result? You get a cmdlet that looks and behaves just like any other PowerShell command, complete with tab completion, help text, parameter binding, and even structured output! But under the hood, it’s still running your old-school CLI.
So, if you’ve got a beloved (or ‘legacy’) command-line tool like netstat, curl, or even a custom corporate CLI, you can wrap it in Crescendo and make it behave like a first-class citizen in PowerShell 😉 (without having to touch C# or PowerShell programming!)
How it looks like
Before Cresendo you would run for querying instance docker like this:
docker ps --all --filter "status=exited"

After crescendo you would run for querying docker instances like this:
Get-DockerContainer -All | Where-Object { $_.Status -like ‘*exited*’ }
Key features
PowerShell Crescendo isn’t just a neat trick, it’s a serious productivity booster for anyone working with command-line tools. It takes the pain out of wrapping CLIs and lets you build polished, professional cmdlets without diving deep into PowerShell or C# internals.
Here are some of the standout features that make Crescendo shine:
- No C# coding required: You don’t need to be a .NET developer to create cmdlets. Everything is defined in a simple JSON configuration file, so you can focus on describing what your command should do rather than writing complex code
- Object output instead of text: Say goodbye to parsing raw text! Crescendo converts command-line output into structured PowerShell objects, making it easy to filter, sort, and process data in the pipeline
- Tab completion on parameters: Your generated cmdlets behave like native PowerShell commands, complete with intuitive tab completion for parameters and arguments
- Pipeline support for chaining commands: Because Crescendo commands output objects, they can seamlessly connect to other PowerShell commands in the pipeline. That means you can build powerful automation flows without messy text parsing
- Integrated help via Get-Help: Each Crescendo module can include full help documentation, examples, and descriptions. Users can discover and understand your cmdlet just like any built-in PowerShell command
- Parameter validation built-in: Add validation rules directly in your JSON config so PowerShell automatically checks input values before execution. It’s cleaner, safer, and more reliable
- Professional modules in minutes: With Crescendo, you can generate production-ready PowerShell modules in a fraction of the time it would take to write them by hand. Ideal for both quick automation tools and enterprise-grade integrations
Getting started
So, we now have the basic introduction to Crescendo, let’s get started!
🎬 Follow the steps below the install crescendo
📒 This is done in PowerShell 7+, I haven’t been testing out 5
- Run the command below to install Crescendo
install-module Microsoft.PowerShell.Crescendo
Nothing to exiting on installing right 😉 let’s get on to the next part and see how we can leverage Crescendo.
On to the next part! 🚀
Our first magic wrapper!
Now Crescendo is installed on your system it’s time to create our first wrapper. For this first Crescendo wrapper we’ll be wrapping “PING”. You heard me correctly! It’s time to put PING under the wings of Crescendo!
📒 I store the JSON schematics for crescendo under a directory in my c:\tools\crescendo drive, I suggest you find a spot for your wrappers as well
🎬 Follow the steps below for the first crescendo wrapper!
- Copy the JSON below to a file ‘ping-crescendo.json’
{
"$schema": "https://aka.ms/PowerShell/Crescendo/Schemas/2021-11",
"Commands": [
{
"$schema": "https://aka.ms/PowerShell/Crescendo/Schemas/2021-11",
"Commands": [
{
"Verb": "Test",
"Noun": "NetworkConnection",
"OriginalName": "ping.exe",
"OriginalCommandElements": [],
"Platform": ["Windows"],
"Description": "Test network connectivity to a host using ICMP echo requests",
"Usage": {
"Synopsis": "Sends ICMP echo requests to test network connectivity"
},
"Parameters": [
{
"Name": "ComputerName",
"OriginalName": "",
"Description": "The computer name or IP address to ping",
"ParameterType": "string",
"Position": 0,
"Mandatory": true
},
{
"Name": "Count",
"OriginalName": "-n",
"Description": "Number of echo requests to send",
"ParameterType": "int",
"DefaultValue": "4"
},
{
"Name": "TimeoutMilliseconds",
"OriginalName": "-w",
"Description": "Timeout in milliseconds to wait for each reply",
"ParameterType": "int"
},
{
"Name": "BufferSize",
"OriginalName": "-l",
"Description": "Send buffer size in bytes",
"ParameterType": "int"
},
{
"Name": "DontFragment",
"OriginalName": "-f",
"Description": "Set Don't Fragment flag in packet (IPv4 only)",
"ParameterType": "switch"
},
{
"Name": "IPv4",
"OriginalName": "-4",
"Description": "Force using IPv4",
"ParameterType": "switch"
},
{
"Name": "IPv6",
"OriginalName": "-6",
"Description": "Force using IPv6",
"ParameterType": "switch"
}
],
"Examples": [
{
"Command": "Test-NetworkConnection -ComputerName google.com",
"Description": "Ping google.com with default count of 4",
"OriginalCommand": "ping.exe google.com -n 4"
},
{
"Command": "Test-NetworkConnection -ComputerName 8.8.8.8 -Count 10",
"Description": "Ping 8.8.8.8 ten times",
"OriginalCommand": "ping.exe 8.8.8.8 -n 10"
},
{
"Command": "Test-NetworkConnection -ComputerName server01 -TimeoutMilliseconds 1000 -Count 5",
"Description": "Ping server01 five times with 1 second timeout",
"OriginalCommand": "ping.exe server01 -n 5 -w 1000"
}
]
}
]
}
- Now in the directory where you stored that JSON file now run the command below
Export-CrescendoModule -ConfigurationFile .\ping-crescendo.json -ModuleName PingModule
You will now see in the directory the module which is created;

📒 Do you see the pattern in the JSON mapping?
{
"Name": "ComputerName",
"OriginalName": "",
"Description": "The computer name or IP address to ping",
"ParameterType": "string",
"Position": 0,
"Mandatory": true
},
Here we are basically telling “ComputerName is now mapped to the first position (as original name is empty)
{
"Name": "Count",
"OriginalName": "-n",
"Description": "Number of echo requests to send",
"ParameterType": "int",
"DefaultValue": "4"
},
Here we map “Count” to “-n” which is of type integer. This is how crescendo makes the mapping for you between your custom parameter names and the ones from the actual program we’re wrapping.
🎬 Now it’s time to test our first wrapped module! Follow the steps below to get started
- Import the module from the directory where crescendo exported with the command below
import-module .\PingModule.psd1
🎉 No errors and warnings! Let’s start testing!
- Run the command to test the wrapper
Test-NetworkConnection -ComputerName google.com -count 10
You will get this result:

Cool right?! It maps perfectly to the PING command! We already have our first PowerShell Wrapper been setup! Although this is very simply wrapped right now it’s doesn’t fully honor Crescendo Still we can’t apply filtering or do pipes which make sense. For that we need to spice it up!
Output handler
When Crescendo needs a little bit steering in order to understand what it should do with the output we can use “output handler”. These output handlers are applied to the output and describe what should be done with the output in order to make it more ‘understandable’
🎬 Follow the steps below to create the first output handler
- Create a file ‘ping-outputhandler.ps1’ and give it the content below
function ConvertFrom-PingOutput {
[CmdletBinding()]
param(
[Parameter(ValueFromPipeline)]
[string[]]$output
)
begin {
$lines = @()
}
process {
$lines += $output
}
end {
# Variables to collect information
$destination = $null
$destinationIP = $null
$replies = @()
$packetsSent = 0
$packetsReceived = 0
$packetsLost = 0
$percentLost = 0
$minTime = 0
$maxTime = 0
$avgTime = 0
foreach ($line in $lines) {
if ([string]::IsNullOrWhiteSpace($line)) {
continue
}
# Parse destination line - handles both hostname and IP formats:
# "Pinging google.com [172.217.14.206] with 32 bytes of data:"
# "Pinging 8.8.8.8 with 32 bytes of data:"
if ($line -match '^Pinging (.+?) \[(.+?)\]') {
# Format with hostname and IP in brackets
$destination = $matches[1]
$destinationIP = $matches[2]
}
elseif ($line -match '^Pinging (.+?) with \d+ bytes') {
# Format with just IP (no brackets)
$destination = $matches[1]
$destinationIP = $matches[1]
}
# Parse reply lines: "Reply from 172.217.14.206: bytes=32 time=15ms TTL=117"
if ($line -match '^Reply from (.+?): bytes=(\d+) time[=<](\d+)ms TTL=(\d+)') {
$replies += [PSCustomObject]@{
From = $matches[1]
Bytes = [int]$matches[2]
Time = [int]$matches[3]
TTL = [int]$matches[4]
Status = 'Success'
}
}
# Handle timeout: "Request timed out."
elseif ($line -match 'Request timed out') {
$replies += [PSCustomObject]@{
From = $null
Bytes = 0
Time = 0
TTL = 0
Status = 'TimedOut'
}
}
# Handle destination host unreachable
elseif ($line -match 'Destination host unreachable') {
$replies += [PSCustomObject]@{
From = $null
Bytes = 0
Time = 0
TTL = 0
Status = 'Unreachable'
}
}
# Parse statistics: "Packets: Sent = 4, Received = 4, Lost = 0 (0% loss),"
if ($line -match 'Packets: Sent = (\d+), Received = (\d+), Lost = (\d+) \((\d+)% loss\)') {
$packetsSent = [int]$matches[1]
$packetsReceived = [int]$matches[2]
$packetsLost = [int]$matches[3]
$percentLost = [int]$matches[4]
}
# Parse round trip times: "Minimum = 14ms, Maximum = 16ms, Average = 15ms"
if ($line -match 'Minimum = (\d+)ms, Maximum = (\d+)ms, Average = (\d+)ms') {
$minTime = [int]$matches[1]
$maxTime = [int]$matches[2]
$avgTime = [int]$matches[3]
}
}
# Return the complete ping result as an object
[PSCustomObject]@{
PSTypeName = 'Network.PingResult'
Destination = $destination
DestinationIP = $destinationIP
Status = if ($packetsReceived -gt 0) { 'Success' } else { 'Failed' }
PacketsSent = $packetsSent
PacketsReceived = $packetsReceived
PacketsLost = $packetsLost
PercentLoss = $percentLost
Replies = $replies
MinimumTime = $minTime
MaximumTime = $maxTime
AverageTime = $avgTime
RoundTripTime = [PSCustomObject]@{
Minimum = $minTime
Maximum = $maxTime
Average = $avgTime
}
IsSuccessful = $packetsReceived -gt 0
HasPacketLoss = $packetsLost -gt 0
}
}
}
📒 This file will help in converting the plain text result from ping into an object. (very simply said; its taking each line of output, strips the content of that line and adds it to the object definition)
You can find more information on handlers here;
- Now update the JSON file ping-cresendo to make sure the handler is getting used:
{
"$schema": "https://aka.ms/PowerShell/Crescendo/Schemas/2021-11",
"Commands": [
{
"Verb": "Test",
"Noun": "NetworkConnection",
"OriginalName": "ping.exe",
"OriginalCommandElements": [],
"Description": "Test network connectivity",
"Parameters": [
{
"Name": "ComputerName",
"OriginalName": "",
"ParameterType": "string",
"Position": 0,
"Mandatory": true
},
{
"Name": "Count",
"OriginalName": "-n",
"ParameterType": "int",
"DefaultValue": "4"
}
],
"OutputHandlers": [
{
"ParameterSetName": "Default",
"Handler": "ConvertFrom-PingOutput",
"HandlerType": "Function"
}
]
}
]
}
- Now run the command to create a new ping-module
export-crescendomodule -ConfigurationFile .\ping-crescendo.json -modulename PingModule -force
- Now import the module again with some force! 😉
import-module .\PingModule.psm1 -force
- And run the code below
$result = Test-NetworkConnection -computername google.com
What really cool is if you now check the $result variable you will see the result below:

That’s cool right! We translated with the outputhandler all the plain text result from the ping command to an object and can now query that object with all PowerShell cmdlets!
Like what I’m showing below:
$result.Replies | Where-Object Time -gt 7

And as it’s powershell we can even supply it with this:
@('google.com', '8.8.8.8', '1.1.1.1') | ForEach-Object {
Test-NetworkConnection -ComputerName $_ -Count 2
} | Select-Object Destination, AverageTime, IsSuccessful | Format-Table
Which will give us the result below:

Sources
If you want to find more information about Crescendo you can check out the resources below. They give you a good overview on all possibilities!
https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.crescendo/?view=ps-modules
Summary
In this post, we explored how PowerShell Crescendo can turn any command-line utility into a fully-featured PowerShell cmdlet, bridging the gap between traditional CLIs and modern PowerShell workflows.
We covered:
🎬 Installing Crescendo using Install-Module Microsoft.PowerShell.Crescendo in PowerShell 7+.
✅ Creating your first wrapper by describing the ping.exe command in a JSON configuration file.
📦 Generating PowerShell modules from the JSON with Export-CrescendoModule, instantly producing cmdlets with parameter validation, help text, and tab completion.
🧩 Transforming output into objects via custom output handlers, making previously plain-text CLI output pipeline-friendly and easily queryable.
📋 Testing and automating commands, showing how to filter, pipe, and summarize results with standard PowerShell cmdlets.
Crescendo empowers developers and IT pros to:
- Wrap legacy or custom CLI tools without writing C# code
- Gain structured, object-oriented output instead of parsing raw text
- Seamlessly integrate commands into PowerShell pipelines for automation
- Build professional, production-ready modules quickly
With Crescendo, what once felt like “1995 text parsing” becomes modern, clean, and pipeline-ready PowerShell magic. Perfect for anyone looking to simplify CLI integration and boost productivity without losing the power of their favorite tools.
Happy wrapping! 😉 And don’t forget having fun doing so!
Oh, In the meantime, let me check if it’s really a Harry Potter Spell! 😁