DEV Community

Anthony Buhnerkemper
Anthony Buhnerkemper

Posted on

Managing Citrix Virtual Apps and Desktops with PowerShell: a practical guide

Citrix Studio is a client of the Citrix PowerShell SDK. Everything you click in Studio turns into SDK calls you could run yourself. Older Studio versions even showed you the PowerShell they ran. So anything Studio can do, PowerShell can do too, plus a lot that Studio can't: bulk changes, scheduled reporting, and changes you can audit and repeat.

This is a condensed version of a longer guide I keep alongside my open-source toolkit. The full guide and all the scripts referenced here are in citrix-powershell-toolkit on GitHub, and the scripts are also packaged as a module on the PowerShell Gallery.

Cmdlet names come from the published SDK, but properties and parameters change between releases. Always check Get-Help <cmdlet> -Full on your own controller and test in a lab.

What the SDK actually talks to

A Citrix Virtual Apps and Desktops (CVAD) site is a set of services on each Delivery Controller, and each service has its own cmdlet prefix:

Service Prefix Examples
Broker Broker Get-BrokerMachine, Get-BrokerSession, Get-BrokerDesktopGroup
Machine Creation Prov Get-ProvScheme, Get-ProvTask, Publish-ProvMasterVMImage
Host Hyp / XDHyp: drive Get-ChildItem XDHyp:\Connections
Configuration Logging Log Get-LogHighLevelOperation
Delegated Admin Admin Get-AdminAdministrator, Get-AdminRole

The cmdlets never touch the site database directly. They make remote calls to a controller, which is why every cmdlet takes -AdminAddress and why you can run the SDK from any domain-joined admin box that has it installed. Licensing is separate: the License Server has its own module (Citrix.Licensing.Admin.V1).

Loading the SDK: snap-ins vs. modules

For years every Citrix service shipped as a Windows PowerShell snap-in, so community scripts are full of:

Add-PSSnapin Citrix*
Enter fullscreen mode Exit fullscreen mode

Snap-ins only work in Windows PowerShell 5.1. From the 2203 release on, the SDK also ships as modules (Citrix.Broker.Admin.V2 and friends), so Import-Module and auto-loading work. Whether PowerShell 7 is supported depends on your release, so test it before you rely on it. In practice, a lot of Citrix automation still runs in 5.1.

My scripts load the SDK defensively, so the same code runs on old and new controllers:

function Import-CitrixSdk {
    if (Get-Command Get-BrokerSite -ErrorAction SilentlyContinue) { return }
    $mod = Get-Module -ListAvailable Citrix.Broker.Admin.V2 | Select-Object -First 1
    if ($mod) { Import-Module $mod -ErrorAction Stop; return }
    if (Get-Command Add-PSSnapin -ErrorAction SilentlyContinue) { Add-PSSnapin Citrix* -ErrorAction SilentlyContinue }
    if (-not (Get-Command Get-BrokerSite -ErrorAction SilentlyContinue)) { throw 'Citrix SDK not found.' }
}
Enter fullscreen mode Exit fullscreen mode

Read Get-Help about_Broker_Filtering at least once. It documents the filter language every Get-Broker* cmdlet shares.

Citrix DaaS: the Remote PowerShell SDK

With Citrix DaaS, Citrix runs the controllers, so you install the Remote PowerShell SDK on a machine you control. The cmdlet names are the same, and the calls go through Citrix Cloud. For automation, create a secure (API) client in the Citrix Cloud console and register it:

Set-XDCredentials -CustomerId 'yourCustomerId' `
                  -SecureClientFile 'C:\Secure\secureclient.csv' `
                  -ProfileType CloudApi -StoreAs 'DaaSAutomation'

# Later sessions
Get-XDCredentials -ProfileName 'DaaSAutomation' | Out-Null
Get-XDAuthentication -ProfileName 'DaaSAutomation'
Enter fullscreen mode Exit fullscreen mode

Treat that secure client file like a password. On DaaS you don't pass -AdminAddress, which is one reason I splat it (see below).

Always say which controller

On-premises, pass -AdminAddress explicitly in scripts. It makes the target obvious and lets the script run from a jump host. I splat it so the same code works with DaaS, where it's omitted:

$ap = @{}
if ($AdminAddress) { $ap.AdminAddress = $AdminAddress }
Get-BrokerDesktopGroup @ap
Enter fullscreen mode Exit fullscreen mode

For scheduled jobs, pick the first healthy controller from a list:

$ddc = 'ddc01','ddc02' | Where-Object {
    try { (Get-BrokerServiceStatus -AdminAddress $_ -ErrorAction Stop).ServiceStatus -eq 'OK' } catch { $false }
} | Select-Object -First 1
Enter fullscreen mode Exit fullscreen mode

Three conventions that bite everyone

1. The 250-record trap. Get-Broker* cmdlets return at most 250 records by default. When there are more, you get a warning and quietly truncated data. This is the most common bug in Citrix reporting scripts. Always set it:

Get-BrokerSession -MaxRecordCount 100000
Enter fullscreen mode Exit fullscreen mode

2. Filter on the server. Typed parameters and -Filter are evaluated by the Broker, so much less data crosses the wire than with Where-Object:

Get-BrokerMachine -RegistrationState Unregistered -InMaintenanceMode $false -MaxRecordCount 5000
Get-BrokerSession -Filter { SessionState -eq 'Disconnected' -and DesktopGroupName -like 'Finance*' } -MaxRecordCount 5000
Enter fullscreen mode Exit fullscreen mode

Save Where-Object for computed values like "disconnected for more than four hours".

3. Names vs. Uids, and three kinds of machine name. Relationships are often exposed as integer Uids (AssociatedDesktopGroupUids on applications), so build a lookup hashtable once instead of querying in a loop. For machines, MachineName is DOMAIN\HOST, DNSName is the FQDN, and HostedMachineName is the hypervisor VM name. Use the right one when you join data.

And the pipeline caution: Set cmdlets accept Get output, which is powerful and easy to over-apply. Run the Get half alone and count the results before adding | Set-BrokerMachine.

The cmdlets I use most

  • Machines — Get-BrokerMachine gives you RegistrationState, PowerState, InMaintenanceMode, SessionCount, FaultState, LastDeregistrationReason, AgentVersion and Tags. Maintenance mode (Set-BrokerMachine -InMaintenanceMode $true) blocks new connections; existing sessions continue.
  • Sessions — Get-BrokerSession with SessionState and SessionStateChangeTime reliably gives you "disconnected since". Stop-BrokerSession logs off, Disconnect-BrokerSession disconnects, and Send-BrokerSessionMessage warns users before maintenance.
  • Delivery groups — Get-BrokerDesktopGroup exposes DesktopsAvailable, DesktopsInUse, DesktopsUnregistered and more, which is enough for a quick capacity dashboard.
  • Power — don't call the hypervisor yourself. Queue New-BrokerHostingPowerAction -Action Restart and let the hosting connection's throttling protect your storage from boot storms.
  • MCS — Get-ProvScheme, Get-ProvTask, and Publish-ProvMasterVMImage for image updates.
  • Configuration Logging — Get-LogHighLevelOperation tells you who changed what and when, whether from Studio or the SDK.

Tags (Add-BrokerTag) deserve special mention. They're the cleanest way to define patch rings or reboot waves without restructuring delivery groups.

Workflows I automate

These map to scripts in the toolkit. Every script that changes something supports -WhatIf and -Confirm.

Morning health check. Get-CitrixMachineHealth (registration, maintenance, power), Get-CitrixDeliveryGroupSummary (capacity), and Get-CitrixSessionReport. Unregistered machines that aren't in maintenance mode are almost always the first thing worth looking at.

Drain a server for patching.

  1. Set-CitrixMaintenanceMode -MachineName ... -Enable $true -WhatIf, then for real.
  2. Optionally warn users with Send-BrokerSessionMessage.
  3. Wait for sessions to drain.
  4. Patch and restart.
  5. Confirm RegistrationState -eq 'Registered', then turn maintenance mode off.

Clean up stale disconnected sessions. Report first with Get-CitrixDisconnectedSessions -MinimumMinutes 480, then act with Invoke-CitrixLogoffIdleSessions -DisconnectedMinutes 480 -WhatIf. Long term, set session-limit timers through policy, so the script handles exceptions rather than doing the work policy should do.

Staggered reboots by tag. Restart-CitrixMachinesByTag skips machines with sessions by default and queues restarts in batches with a delay. For multi-session groups, consider the built-in reboot schedules too.

MCS image update. Update and seal the master, snapshot it, Publish-ProvMasterVMImage, watch Get-ProvTask, then roll machines onto the new image with a staged reboot.

Documentation and audit. Export-CitrixSiteDocumentation writes a point-in-time HTML/CSV snapshot of the site. Get-CitrixConfigLogReport pulls Configuration Logging entries. Get-CitrixCatalogReport, Get-CitrixApplicationInventory and Get-CitrixLicenseUsage round out the inventory.

A note on reporting output

Every report in the toolkit emits objects first and files second. That sounds minor, but it means you can pipe a report into Where-Object, Group-Object or Export-Csv the same way you would any other cmdlet, and only write HTML or CSV when you actually want an artifact. For example, a quick count of unregistered machines per delivery group:

Get-CitrixMachineHealth -AdminAddress ddc01 |
    Where-Object RegistrationState -ne 'Registered' |
    Group-Object DesktopGroupName | Sort-Object Count -Descending
Enter fullscreen mode Exit fullscreen mode

Objects also make it easy to diff today's run against yesterday's, which is often where the useful signal is.

Safe change practice

The habits that have saved me the most pain:

  • -WhatIf first, every time. If a script that changes state doesn't support ShouldProcess, don't schedule it.
  • Get, count, then Set. (Get-BrokerMachine -Tag 'Wave1' -MaxRecordCount 5000).Count before you pipe anything.
  • Small batches. Pilot on one machine, then one delivery group, then everything.
  • Leave a trail. SDK changes appear in Configuration Logging; add your own transcript or log file for scheduled jobs.

Scheduling and least privilege

For scheduled tasks, run under a dedicated service account with a custom Citrix delegated administration role scoped to only what the job needs. A read-only reporting job should never run as Full Administrator. On DaaS, use an API client per automation, so you can rotate or revoke one without breaking the others. Run Windows PowerShell 5.1 unless you've confirmed your SDK release supports 7.

Troubleshooting quick hits

  • "The term Get-BrokerMachine is not recognized" — SDK not installed or not loaded. Check Get-Module -ListAvailable Citrix* and Get-PSSnapin -Registered Citrix*.
  • Exactly 250 results — you forgot -MaxRecordCount.
  • Access denied on some objects — your delegated admin scope doesn't cover them.
  • New property missing — your SDK is older than your controllers. Upgrade the SDK on the admin box.

Get the code

Everything here, plus the full long-form guide, is MIT-licensed:

If you find a property that changed in your release, open an issue. Version drift is the hardest part of keeping Citrix automation accurate.

Top comments (1)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.