Mass Agent Deployment: From 500 Machines to Done in Under an Hour

Deploy Automate agents to 500 computers in under an hour. This PowerShell script runs parallel deployments with error handling and verification. Complete code included for production use.

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

  1. Test on pilot group first - Deploy to 5-10% before full rollout
  2. Enable WinRM ahead of time - Use GPO to enable PowerShell remoting
  3. Stagger deployments - Don’t hammer your Automate server
  4. Monitor server load - Watch server resources during deployment
  5. Keep installer tokens secure - Don’t commit to Git, use secure vaults
  6. Verify check-ins - Confirm agents appear in Automate console
  7. Document LocationIDs - Keep a mapping of sites to IDs
  8. 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

Next in series: Troubleshooting ConnectWise Automate agents like a pro with PowerShell diagnostics.

Comments