PowerCLI for vSphere Tags and Custom Attributes: Metadata Automation Across Your Inventory

Quick answer: scope the query, then review the change

Use tags for category-based classification and custom attributes for named values. For exports, read the assignment’s Tag object and its Category and Name properties rather than expecting a top-level Category field.

Jump to tag creation and single-VM assignment, bulk review, CSV export, or custom attributes.

Before running the examples

Use a compatible PowerCLI installation, a trusted vCenter certificate, and an account with only the permissions required. Run this connection setup and helper once in a fresh interactive session; replace the example hostname. Every later query explicitly uses $vc.

$ErrorActionPreference = 'Stop'
$vc = Connect-VIServer -Server 'vcenter.example.com' -Credential (Get-Credential) -ErrorAction Stop

function Get-One {
    param([object[]]$Items, [string]$Label)
    if (@($Items).Count -ne 1) {
        throw "Expected exactly one $Label; found $(@($Items).Count)."
    }
    $Items[0]
}

Run examples individually. Creation commands are commented out until you review and intentionally uncomment them; assignment and value changes default to $ApplyChanges = $false. These are local safety guards, not server-side transactions or a vendor -WhatIf mode. Confirm object IDs and downstream policy impact before enabling any write. After a change, rerun the relevant read-only query to confirm the result.

References: category-scoped tag lookup, tag assignment, category cardinality, custom-attribute target types, and annotation queries.

Introduction

Tags and custom attributes bring metadata structure to your VMware environment. Whether you’re classifying workloads, enforcing policies, or generating reports, PowerCLI enables full automation of tag and attribute management.

In this article, you will learn how to:

  • Create and organize tag categories
  • Assign tags to VMs, hosts, and other objects
  • Query tags for filtering and automation
  • Manage custom attributes and annotations
  • Export tag metadata for documentation

My Personal Repository on GitHub

VMware Repository on GitHub


Understanding Tags vs Custom Attributes

FeatureTagsCustom attributes
PurposeClassification labels organized into categoriesNamed values attached to inventory objects
ScopeThe category’s entity types control where a tag can be assignedThe attribute’s target type controls its scope; supported objects include VMs, hosts, and other inventory types
MultiplicitySingle or Multiple cardinality is defined on the categoryOne value per attribute on an entity
Typical useApplication, environment, service tier, or policy selectionOwner, reference ID, cost center, or operational notes
PowerCLIGet-Tag, Get-TagAssignment, New-TagAssignmentGet-CustomAttribute, Get-Annotation, Set-Annotation

Tag Management with PowerCLI

Create a Tag Category

# Inspect first. Creating a category changes vCenter.
Get-TagCategory -Server $vc | Where-Object { $_.Name -eq 'ApplicationType' }

# Uncomment only after confirming that a new category is needed.
# New-TagCategory -Server $vc -Name 'ApplicationType' -Cardinality Single -EntityType VirtualMachine -ErrorAction Stop

Create Tags Inside the Category

$category = Get-One -Items @(Get-TagCategory -Server $vc -Name 'ApplicationType') -Label 'ApplicationType category'
Get-Tag -Server $vc -Category $category

# Create only missing, approved tags; uncomment the relevant line.
# New-Tag -Server $vc -Name 'WebApp' -Category $category -ErrorAction Stop
# New-Tag -Server $vc -Name 'Database' -Category $category -ErrorAction Stop

Assign a Tag to a VM

$ApplyChanges = $false  # Set true only after reviewing this VM and its current tags.
$vm = Get-One -Items @(Get-VM -Server $vc -Name 'SQL01') -Label 'SQL01 VM'
$category = Get-One -Items @(Get-TagCategory -Server $vc -Name 'ApplicationType') -Label 'ApplicationType category'
$tag = Get-One -Items @(Get-Tag -Server $vc -Category $category -Name 'Database') -Label 'Database tag'
$current = @(Get-TagAssignment -Server $vc -Entity $vm -Category $category)
$current | Select-Object @{N='VM';E={$vm.Name}}, @{N='Tag';E={$_.Tag.Name}}

if (@($current | ForEach-Object { $_.Tag.Id }) -contains $tag.Id) {
    Write-Output 'The requested tag is already assigned.'
} elseif ($current.Count -gt 0 -and $category.Cardinality -eq 'Single') {
    throw 'A different tag already occupies this Single category. Review it; do not replace it automatically.'
} elseif ($ApplyChanges) {
    New-TagAssignment -Server $vc -Entity $vm -Tag $tag -ErrorAction Stop
} else {
    Write-Output "Preview only: assign $($tag.Name) to $($vm.Name) [$($vm.Id)]."
}

List Tags for a VM

$vm = Get-One -Items @(Get-VM -Server $vc -Name 'SQL01') -Label 'SQL01 VM'
Get-TagAssignment -Server $vc -Entity $vm |
    Select-Object @{N='VM';E={$vm.Name}}, @{N='Category';E={$_.Tag.Category.Name}}, @{N='Tag';E={$_.Tag.Name}}

Bulk Tag Assignment Using Filters

Resolve one folder and preview its VM IDs before assigning anything. This example includes VMs in child folders. It refuses an empty target list and stops if a different tag already occupies the Single category. Tag changes can alter downstream backup or policy selection, so obtain the relevant owner’s approval.

$ApplyChanges = $false  # Keep false until the complete target list is approved.
$folder = Get-One -Items @(Get-Folder -Server $vc -Type VM -Name 'Web Servers') -Label 'Web Servers VM folder'
$category = Get-One -Items @(Get-TagCategory -Server $vc -Name 'ApplicationType') -Label 'ApplicationType category'
$tag = Get-One -Items @(Get-Tag -Server $vc -Category $category -Name 'WebApp') -Label 'WebApp tag'
$targets = @(Get-VM -Server $vc -Location $folder)
if ($targets.Count -eq 0) { throw 'No VMs matched; nothing will be changed.' }

$pending = @(
    foreach ($vm in $targets) {
        $current = @(Get-TagAssignment -Server $vc -Entity $vm -Category $category)
        if (@($current | ForEach-Object { $_.Tag.Id }) -contains $tag.Id) { continue }
        if ($current.Count -gt 0 -and $category.Cardinality -eq 'Single') {
            throw "Conflicting category assignment on $($vm.Name) [$($vm.Id)]."
        }
        $vm
    }
)
$targets | Select-Object Name, Id
Write-Output "$($targets.Count) VMs in scope; $($pending.Count) need the tag."

if ($ApplyChanges -and $pending.Count -gt 0) {
    foreach ($vm in $pending) {
        New-TagAssignment -Server $vc -Entity $vm -Tag $tag -ErrorAction Stop
    }
}

For guest-OS-based discovery, start with a read-only candidate list. Guest-reported OS data may be unavailable or stale; review the VM identities before using the assignment pattern above. Do not pass an unverified tag name directly into a bulk write.

Get-VM -Server $vc |
    Where-Object { $_.Guest.OSFullName -like '*Ubuntu*' } |
    Select-Object Name, Id, @{N='GuestOS';E={$_.Guest.OSFullName}}

Exporting Tag Assignments to CSV

# Read-only vCenter query; writes only a local CSV.
# One row per assignment. Untagged VMs do not appear in this report.
$rows = @(
    foreach ($vm in @(Get-VM -Server $vc)) {
        foreach ($assignment in @(Get-TagAssignment -Server $vc -Entity $vm)) {
            [pscustomobject]@{
                vCenter  = $vc.Name
                VM       = $vm.Name
                VMId     = $vm.Id
                Category = $assignment.Tag.Category.Name
                Tag      = $assignment.Tag.Name
            }
        }
    }
)
if ($rows.Count -eq 0) { throw 'No tag assignments were returned; no CSV was written.' }

$reportPath = 'C:\Reports\VM_Tags.csv'
$reportDirectory = Split-Path -Path $reportPath -Parent
if (-not (Test-Path -LiteralPath $reportDirectory -PathType Container)) {
    New-Item -ItemType Directory -Path $reportDirectory -ErrorAction Stop | Out-Null
}
# Choose a new filename for subsequent exports; never overwrite an existing report.
$rows | Export-Csv -LiteralPath $reportPath -NoTypeInformation -Encoding UTF8 -NoClobber

Managing Custom Attributes

List Existing Attributes

Get-CustomAttribute -Server $vc

Create a New Attribute

Get-CustomAttribute -Server $vc | Where-Object { $_.Name -eq 'Owner' }

# Uncomment only if an approved VM-scoped Owner attribute does not already exist.
# New-CustomAttribute -Server $vc -Name 'Owner' -TargetType VirtualMachine -ErrorAction Stop

Set Value on a VM

$ApplyChanges = $false
$vm = Get-One -Items @(Get-VM -Server $vc -Name 'SQL01') -Label 'SQL01 VM'
$ownerAttribute = Get-One -Items @(Get-CustomAttribute -Server $vc -Name 'Owner') -Label 'Owner attribute'
Get-Annotation -Server $vc -Entity $vm -CustomAttribute $ownerAttribute

# Confirm the attribute applies to this VM and that replacing its value is intended.
if ($ApplyChanges) {
    Set-Annotation -Server $vc -Entity $vm -CustomAttribute $ownerAttribute -Value 'TeamA' -ErrorAction Stop
} else {
    Write-Output "Preview only: set Owner to TeamA on $($vm.Name) [$($vm.Id)]."
}

Report Custom Attribute Values

$ownerAttribute = Get-One -Items @(Get-CustomAttribute -Server $vc -Name 'Owner') -Label 'Owner attribute'
Get-VM -Server $vc | ForEach-Object {
    $vm = $_
    $owner = @(Get-Annotation -Server $vc -Entity $vm -CustomAttribute $ownerAttribute)
    [pscustomobject]@{
        VM = $vm.Name
        VMId = $vm.Id
        Owner = ($owner.Value -join '; ')
    }
}

Diagram: Tag and Attribute Automation

Diagram of Tag and Attribute Automation.

Use Case: Backup Classification by Tag

You can tag VMs as Daily, Weekly, or Excluded and use backup tools to process workloads accordingly.

Before assigning a backup-selection tag, agree on its category, permitted values, and meaning with the backup owner. The following discovery step assumes an existing category named BackupPolicy; it makes no changes. Use the guarded assignment pattern above only after reviewing any current policy tag.

$vm = Get-One -Items @(Get-VM -Server $vc -Name 'AppServer01') -Label 'AppServer01 VM'
$backupCategory = Get-One -Items @(Get-TagCategory -Server $vc -Name 'BackupPolicy') -Label 'BackupPolicy category'
$dailyTag = Get-One -Items @(Get-Tag -Server $vc -Category $backupCategory -Name 'Daily') -Label 'Daily backup tag'
Get-TagAssignment -Server $vc -Entity $vm -Category $backupCategory
$dailyTag | Select-Object Name, @{N='Category';E={$_.Category.Name}}

A tag does not create or prove a backup by itself. Confirm how your backup product consumes tags, verify that the intended VM is included in the right job or policy, and test the resulting recovery workflow.


Troubleshooting

SymptomCheck before changing anything
No match or multiple matchesCheck the vCenter, object ID, category, and folder. The Get-One helper deliberately stops when a name does not resolve to exactly one object.
Tag assignment rejectedCheck permissions, the category’s supported entity types, and its Single/Multiple cardinality. Do not silently remove a conflicting assignment.
Blank Category column or object names in CSVRead the category through assignment.Tag.Category.Name and the tag through assignment.Tag.Name.
Missing custom-attribute valueResolve the correct attribute and entity, check the attribute target type, and use Get-Annotation. An unset value can legitimately be blank.
UI and script disagreeRe-query the same object ID and vCenter, refresh the browser, and investigate permissions or API errors. A client restart is not a universal repair.
Bulk run stopped partwayWrites are not a transaction. Inspect current assignments before resuming; do not assume earlier writes rolled back.

What’s Next

Next article will focus on:

  • Generating compliance and health reports using PowerCLI
  • Scheduling reports for VM state, orphaned snapshots, and configuration drift

Keep exploring

Choose your next step

Continue with the path that best matches the architecture or operating challenge in front of you.

Leave a Reply

Discover more from Digital Thought Disruption

Subscribe now to keep reading and get access to the full archive.

Continue reading