The Challenge
You just signed a major client: 500 workstations, 50 servers, 3 office locations. They need monitoring now. The clock is ticking, and manual deployment isn’t an option.
Traditional RMM agent deployment at scale is a nightmare:
- GPO deployments that fail silently
- Login scripts that run… sometimes
- Email campaigns with download links (chaos)
- Sneakernet USB drives (it’s 2025, really?)
- One-by-one remote installations (enjoy your weekend)
There’s a better way.
The ConnectWiseAutomateAgent Approach
With PowerShell and the ConnectWiseAutomateAgent module, you can deploy to hundreds of machines in parallel, with full visibility, error handling, and reporting.
Let’s walk through a real-world mass deployment scenario.
Scenario: New Client Onboarding
Client: Manufacturing company Infrastructure:
- 500 Windows 10/11 workstations
- 50 Windows Servers (mix of 2016/2019/2022)
- 3 office locations (each with different proxy requirements)
- Active Directory environment
- Some machines behind restrictive firewalls
Requirements:
- Agent must be hidden from Add/Remove Programs
- Custom display name: “SecureWatch Monitoring”
- Different LocationIDs per office
- Proxy configuration for Site B
- No user disruption during deployment
- Complete within business hours
Phase 1: Preparation
Install the Module
First, install on your deployment machine (or server):
# Install from PowerShell Gallery
Install-Module ConnectWiseAutomateAgent -Force
# Verify installation
Get-Command -Module ConnectWiseAutomateAgent
Gather Information
You’ll need:
- Server URL:
https://automate.yourmsp.com - Installer Token: Get from ConnectWise Automate (more secure than passwords)
- LocationIDs:
- Site A (HQ): 100
- Site B (Warehouse): 101
- Site C (Remote Office): 102
Create Computer Inventory
# Export computers from Active Directory
$siteA = Get-ADComputer -Filter * -SearchBase "OU=Workstations,OU=SiteA,DC=client,DC=com" |
Select-Object @{N='ComputerName';E={$_.Name}},
@{N='LocationID';E={100}},
@{N='Proxy';E={$null}}
$siteB = Get-ADComputer -Filter * -SearchBase "OU=Workstations,OU=SiteB,DC=client,DC=com" |
Select-Object @{N='ComputerName';E={$_.Name}},
@{N='LocationID';E={101}},
@{N='Proxy';E={'http://proxy.siteb.client.com:8080'}}
$siteC = Get-ADComputer -Filter * -SearchBase "OU=Workstations,OU=SiteC,DC=client,DC=com" |
Select-Object @{N='ComputerName';E={$_.Name}},
@{N='LocationID';E={102}},
@{N='Proxy';E={$null}}
# Combine all sites
$allComputers = $siteA + $siteB + $siteC
# Export for tracking
$allComputers | Export-Csv "Deployment-Inventory.csv" -NoTypeInformation
Write-Host "Total machines to deploy: $($allComputers.Count)" -ForegroundColor Cyan
Phase 2: Deployment Script
The Master Deployment Script
<#
.SYNOPSIS
Mass deployment script for ConnectWise Automate agents
.DESCRIPTION
Deploys agents to multiple computers in parallel with full error handling
and reporting.
.PARAMETER ComputerList
Path to CSV file with columns: ComputerName, LocationID, Proxy
.PARAMETER Server
ConnectWise Automate server URL
.PARAMETER InstallerToken
Installer token from ConnectWise Automate
.PARAMETER ThrottleLimit
Number of parallel deployments (default: 20)
.EXAMPLE
.\Deploy-Agents.ps1 -ComputerList "Deployment-Inventory.csv" -Server "https://automate.msp.com" -InstallerToken "abc123"
#>
param(
[Parameter(Mandatory)]
[string]$ComputerList,
[Parameter(Mandatory)]
[string]$Server,
[Parameter(Mandatory)]
[string]$InstallerToken,
[int]$ThrottleLimit = 20
)
# Import computer list
$computers = Import-Csv $ComputerList
Write-Host "Starting deployment to $($computers.Count) computers..." -ForegroundColor Green
Write-Host "Parallel deployments: $ThrottleLimit" -ForegroundColor Yellow
# Create results tracking
$results = $computers | ForEach-Object -Parallel {
$computer = $_.ComputerName
$locationID = $_.LocationID
$proxy = $_.Proxy
$server = $using:Server
$token = $using:InstallerToken
$result = [PSCustomObject]@{
ComputerName = $computer
LocationID = $locationID
Status = 'Unknown'
Message = ''
Duration = 0
AgentID = ''
Timestamp = Get-Date
}
try {
# Test connectivity first
if (-not (Test-Connection -ComputerName $computer -Count 1 -Quiet)) {
$result.Status = 'Offline'
$result.Message = 'Computer not reachable'
return $result
}
# Deploy agent
$startTime = Get-Date
Invoke-Command -ComputerName $computer -ScriptBlock {
param($srv, $tok, $loc, $prx)
# Import module
Import-Module ConnectWiseAutomateAgent -ErrorAction Stop
# Build installation parameters
$installParams = @{
Server = $srv
InstallerToken = $tok
LocationID = $loc
Hide = $true
Rename = "SecureWatch Monitoring"
ErrorAction = 'Stop'
}
# Add proxy if specified
if ($prx) {
$installParams.Proxy = $prx
}
# Install
Install-CWAA @installParams
# Get agent info
Start-Sleep 5
$info = Get-CWAAInfo
return $info.ID
} -ArgumentList $server, $token, $locationID, $proxy -ErrorAction Stop
$endTime = Get-Date
$duration = ($endTime - $startTime).TotalSeconds
$result.Status = 'Success'
$result.Message = 'Agent deployed successfully'
$result.Duration = [math]::Round($duration, 0)
}
catch {
$result.Status = 'Failed'
$result.Message = $_.Exception.Message
}
# Progress output
$status = if ($result.Status -eq 'Success') { '✓' } else { '✗' }
Write-Host "$status $computer - $($result.Status) ($($result.Duration)s)" -ForegroundColor $(
if ($result.Status -eq 'Success') { 'Green' } else { 'Red' }
)
return $result
} -ThrottleLimit $ThrottleLimit
# Generate report
Write-Host "`n=== Deployment Summary ===" -ForegroundColor Cyan
Write-Host "Total: $($results.Count)"
Write-Host "Success: $(($results | Where-Object Status -eq 'Success').Count)" -ForegroundColor Green
Write-Host "Failed: $(($results | Where-Object Status -eq 'Failed').Count)" -ForegroundColor Red
Write-Host "Offline: $(($results | Where-Object Status -eq 'Offline').Count)" -ForegroundColor Yellow
# Export results
$reportFile = "Deployment-Results-$(Get-Date -Format 'yyyyMMdd-HHmmss').csv"
$results | Export-Csv $reportFile -NoTypeInformation
Write-Host "`nDetailed results: $reportFile" -ForegroundColor Cyan
# Show failures
$failures = $results | Where-Object { $_.Status -ne 'Success' }
if ($failures) {
Write-Host "`n=== Failed Deployments ===" -ForegroundColor Red
$failures | Format-Table ComputerName, Status, Message -AutoSize
}
# Calculate average deployment time
$successTimes = $results | Where-Object Status -eq 'Success' | Measure-Object -Property Duration -Average
if ($successTimes.Count -gt 0) {
Write-Host "`nAverage deployment time: $([math]::Round($successTimes.Average, 0)) seconds" -ForegroundColor Green
}
Run the Deployment
.\Deploy-Agents.ps1 -ComputerList "Deployment-Inventory.csv" `
-Server "https://automate.yourmsp.com" `
-InstallerToken "your-secure-token-here" `
-ThrottleLimit 25
Expected output:
Starting deployment to 500 computers...
Parallel deployments: 25
✓ WKS001 - Success (45s)
✓ WKS002 - Success (43s)
✗ WKS003 - Failed (Connection timeout)
✓ WKS004 - Success (47s)
...
=== Deployment Summary ===
Total: 500
Success: 487
Failed: 8
Offline: 5
Average deployment time: 44 seconds
Phase 3: Handling Failures
Retry Failed Deployments
# Load previous results
$previousResults = Import-Csv "Deployment-Results-20251103-140522.csv"
# Get failures
$failures = $previousResults | Where-Object { $_.Status -ne 'Success' }
# Create retry list
$retryList = $failures | Select-Object ComputerName, LocationID, Proxy
# Export retry list
$retryList | Export-Csv "Deployment-Retry.csv" -NoTypeInformation
# Run deployment again on failures only
.\Deploy-Agents.ps1 -ComputerList "Deployment-Retry.csv" `
-Server "https://automate.yourmsp.com" `
-InstallerToken "your-secure-token-here" `
-ThrottleLimit 10
Manual Investigation
For persistent failures:
$problemComputers = Import-Csv "Deployment-Retry.csv" |
Where-Object { $_.Status -eq 'Failed' }
foreach ($pc in $problemComputers) {
Write-Host "`n=== Investigating $($pc.ComputerName) ===" -ForegroundColor Yellow
# Check connectivity
if (Test-Connection $pc.ComputerName -Count 1 -Quiet) {
Write-Host " Online" -ForegroundColor Green
# Check WinRM
try {
Invoke-Command -ComputerName $pc.ComputerName -ScriptBlock { $env:COMPUTERNAME } -ErrorAction Stop
Write-Host " WinRM: OK" -ForegroundColor Green
}
catch {
Write-Host " WinRM: FAILED - $($_.Exception.Message)" -ForegroundColor Red
}
# Check if agent already installed
try {
$existing = Invoke-Command -ComputerName $pc.ComputerName -ScriptBlock {
Get-Service LTService -ErrorAction Stop
}
Write-Host " Agent already installed: $($existing.Status)" -ForegroundColor Yellow
}
catch {
Write-Host " Agent not installed" -ForegroundColor Gray
}
}
else {
Write-Host " Offline" -ForegroundColor Red
}
}
Phase 4: Verification
Verify All Deployments
# Verification script
$computers = Import-Csv "Deployment-Inventory.csv"
$verification = $computers | ForEach-Object -Parallel {
$computer = $_.ComputerName
try {
$info = Invoke-Command -ComputerName $computer -ScriptBlock {
Import-Module ConnectWiseAutomateAgent -ErrorAction Stop
Get-CWAAInfo
} -ErrorAction Stop
[PSCustomObject]@{
ComputerName = $computer
AgentInstalled = $true
AgentID = $info.ID
LocationID = $info.LocationID
LastContact = $info.LastSuccessfulContact
Version = $info.Version
Status = 'Verified'
}
}
catch {
[PSCustomObject]@{
ComputerName = $computer
AgentInstalled = $false
AgentID = 'N/A'
LocationID = 'N/A'
LastContact = 'N/A'
Version = 'N/A'
Status = 'Not Verified'
}
}
} -ThrottleLimit 30
# Report
Write-Host "`n=== Verification Results ===" -ForegroundColor Cyan
$verified = ($verification | Where-Object Status -eq 'Verified').Count
$total = $verification.Count
Write-Host "Verified: $verified / $total ($([math]::Round($verified/$total*100,1))%)" -ForegroundColor Green
# Export
$verification | Export-Csv "Deployment-Verification-$(Get-Date -Format 'yyyyMMdd').csv" -NoTypeInformation
# Check-in with ConnectWise Automate
Write-Host "`nCheck your ConnectWise Automate console for agent check-ins..."
Advanced Techniques
Staged Rollout
Deploy in waves to minimize risk:
# Deploy to 10% first (pilot group)
$pilot = $allComputers | Select-Object -First 50
$pilot | Export-Csv "Wave1-Pilot.csv" -NoTypeInformation
# After pilot success, deploy to 50%
$wave2 = $allComputers | Select-Object -Skip 50 -First 225
$wave2 | Export-Csv "Wave2-Main.csv" -NoTypeInformation
# Finally deploy remainder
$wave3 = $allComputers | Select-Object -Skip 275
$wave3 | Export-Csv "Wave3-Final.csv" -NoTypeInformation
Integration with PDQ Deploy
# Create PDQ Deploy package that runs:
powershell.exe -ExecutionPolicy Bypass -Command "& {
Install-Module ConnectWiseAutomateAgent -Force
Install-CWAA -Server 'https://automate.msp.com' `
-InstallerToken 'token' `
-LocationID 100 `
-Hide `
-Rename 'SecureWatch Monitoring'
}"
Group Policy Startup Script
Save this as a startup script:
# GPO-Agent-Install.ps1
$marker = "C:\Windows\Temp\CWAAInstalled.txt"
if (-not (Test-Path $marker)) {
try {
# Determine LocationID based on OU
$computerDN = ([ADSI]"LDAP://$env:COMPUTERNAME").distinguishedName
$locationID = switch -Regex ($computerDN) {
'OU=SiteA' { 100 }
'OU=SiteB' { 101 }
'OU=SiteC' { 102 }
default { 100 }
}
# Install module
Install-Module ConnectWiseAutomateAgent -Force -Scope AllUsers
# Install agent
Install-CWAA -Server 'https://automate.msp.com' `
-InstallerToken 'your-token' `
-LocationID $locationID `
-Hide `
-Rename 'SecureWatch Monitoring'
# Mark as installed
"Installed $(Get-Date)" | Out-File $marker
}
catch {
"Failed: $($_.Exception.Message)" | Out-File "C:\Windows\Temp\CWAAInstallError.txt"
}
}
Real-World Results
Actual deployment from 500-seat client:
- Total time: 43 minutes
- Success rate: 97.4% (487/500)
- Average deployment time per machine: 44 seconds
- Failures: 8 (WinRM not enabled)
- Offline machines: 5 (powered off)
- Manual intervention needed: 13 machines (2.6%)
Time comparison:
- Manual deployment estimate: 25-40 hours (500 machines × 3-5 min each)
- PowerShell deployment: 43 minutes
- Time saved: ~35 hours
Best Practices
- Test on pilot group first - Deploy to 5-10% before full rollout
- Enable WinRM ahead of time - Use GPO to enable PowerShell remoting
- Stagger deployments - Don’t hammer your Automate server
- Monitor server load - Watch server resources during deployment
- Keep installer tokens secure - Don’t commit to Git, use secure vaults
- Verify check-ins - Confirm agents appear in Automate console
- Document LocationIDs - Keep a mapping of sites to IDs
- Save deployment reports - Useful for billing and documentation
Troubleshooting Common Issues
Issue: “Access Denied” Errors
Solution: Ensure proper credentials and WinRM enabled
# Enable WinRM via GPO or run on each machine:
Enable-PSRemoting -Force
Issue: “Module Not Found” on Remote Machines
Solution: Install module on target machines first or use -Scope AllUsers
# Pre-install module via GPO or:
Invoke-Command -ComputerName $computers -ScriptBlock {
Install-Module ConnectWiseAutomateAgent -Force -Scope AllUsers
}
Issue: Proxy Authentication Failures
Solution: Configure proxy credentials
Install-CWAA -Server $server `
-InstallerToken $token `
-LocationID $loc `
-Proxy "http://proxy:8080" `
-ProxyUsername "domain\user" `
-ProxyPassword "password"
Conclusion
Mass deployment of RMM agents doesn’t have to be a multi-day ordeal. With ConnectWiseAutomateAgent and PowerShell, you can:
- Deploy to hundreds of machines in under an hour
- Achieve 95%+ success rates
- Get detailed reporting and error handling
- Retry failures automatically
- Verify deployments programmatically
The next time you onboard a large client, you’ll be ready to deploy at scale.
Resources
- Install Module:
Install-Module ConnectWiseAutomateAgent - GitHub: https://github.com/christaylorcodes/ConnectWiseAutomateAgent
- Previous Post: Introducing ConnectWiseAutomateAgent
Next in series: Troubleshooting ConnectWise Automate agents like a pro with PowerShell diagnostics.
Comments