Skip to main content

Items Field Inspection & Bulk Updates

PowerShell utilities for inspecting Items API fields and performing bulk updates via HaloPSA API.

Overview

These scripts provide field-level inspection and bulk update capabilities for the Items (Inventory) module. Useful for troubleshooting, data cleanup, and mass modifications.

⚠️ CRITICAL: Pagination Requirement

The HaloPSA Items API requires pageinate=true parameter for pagination to function.

Without this parameter, the API will:

    Ignore page_no and page_size parameters Return only the first ~1,000 items regardless of page number Return identical results on all page requests Provide no error or warning

    Required Query Format

    # CORRECT - Pagination enabled
    $itemsUrl = "$apiUrl/Item?pageinate=true&page_no=$page&page_size=$pageSize"
    
    # WRONG - Pagination disabled (parameters ignored)
    $itemsUrl = "$apiUrl/Item?page_no=$page&page_size=$pageSize"
    

    Impact

    If you omit pageinate=true:

      Scripts will only process the first ~1,000 items Remaining items will never be retrieved or updated API provides no indication of missing data Updates will appear successful but be incomplete

      Always verify total item counts match expected values after bulk operations.

      Verification Test

      # Test pagination is working
      $testUrl = "$apiUrl/Item?pageinate=true&page_no=2&page_size=100"
      $response = Invoke-RestMethod -Uri $testUrl -Headers $headers -Method Get
      
      # Page 2 should have different items than page 1
      Write-Host "Page 2 items: $($response.items.Count)"
      Write-Host "First item ID: $($response.items[0].id)"
      

      If first item ID on page 2 is 101 (not 1), pagination is working correctly.

      Field Inspection Script

      Query all fields available on an Item record to identify field names, types, and current values.

      Use Cases

      • Discover field names before writing update scripts
      • Troubleshoot missing or unexpected data
      • Verify field types for API payload construction
      • Document available fields for integration work

      Script

      <#
      .SYNOPSIS
          HaloPSA Items - Field Inspector
      .DESCRIPTION
          Queries Items API and displays all available fields with types and values
      #>
      
      # Configuration
      $tenant = 'your_tenant'
      $clientId = 'your_client_id'
      $clientSecret = 'your_client_secret'
      
      $authUrl = "https://$tenant.halopsa.com/auth/token"
      $apiUrl = "https://$tenant.halopsa.com/api"
      
      # Authenticate
      $tokenBody = @{
          grant_type = 'client_credentials'
          client_id = $clientId
          client_secret = $clientSecret
          scope = 'read:items'
      }
      
      $token = Invoke-RestMethod -Uri $authUrl -Method Post -Body $tokenBody
      $headers = @{
          Authorization = "Bearer $($token.access_token)"
          'Content-Type' = 'application/json'
      }
      
      # Query Items with detailed fields
      $itemsUrl = "$apiUrl/Item?page_size=5&includedetails=true"
      $response = Invoke-RestMethod -Uri $itemsUrl -Headers $headers -Method Get
      
      # Display field information
      foreach ($item in $response.items) {
          Write-Host "`n=== Item ID: $($item.id) - $($item.name) ===" -ForegroundColor Cyan
          
          $item.PSObject.Properties | ForEach-Object {
              $fieldName = $_.Name
              $fieldValue = $_.Value
              $fieldType = $_.TypeNameOfValue
              
              Write-Host "$fieldName" -NoNewline -ForegroundColor Yellow
              Write-Host " [$fieldType]" -NoNewline -ForegroundColor Gray
              Write-Host " = $fieldValue" -ForegroundColor White
          }
      }
      
      # Save raw JSON for detailed inspection
      $response | ConvertTo-Json -Depth 10 | Out-File "items_fields_raw.json"
      Write-Host "`nRaw JSON saved to: items_fields_raw.json" -ForegroundColor Green
      

      Key Parameters

      Parameter Description
      includedetails=true Returns all fields including custom fields
      page_size Number of items to return (use small number for inspection)

      Example Output

      === Item ID: 363 - MNT-135 ===
      id [Int32] = 363
      name [String] = MNT-135
      description [String] = Tilting Wall Mount
      template_id [Int32] = 88
      item_type_id [Int32] = 2
      supplier_id [Int32] = 15
      ...
      

      Bulk Update Script

      Perform mass updates on Items records with dry-run mode, batch processing, and comprehensive logging.

      Use Cases

      • Clear template_id from all items
      • Update pricing across inventory
      • Reassign supplier relationships
      • Bulk field modifications

      Features

      • Dry-run mode by default - Preview changes before execution
      • Batch processing - Configurable batch sizes with rate limiting
      • Progress tracking - Real-time percentage complete
      • Error handling - Continues on failures, reports all errors
      • Comprehensive logging - Full audit trail with timestamps

      Script

      <#
      .SYNOPSIS
          HaloPSA Items - Bulk Update Template
      .DESCRIPTION
          Bulk update Items with dry-run mode and comprehensive logging
          Modify the $updatePayload section for your specific use case
      #>
      
      $ErrorActionPreference = 'Continue'
      
      # ============================================
      # CONFIGURATION
      # ============================================
      $tenant = 'your_tenant'
      $clientId = 'your_client_id'
      $clientSecret = 'your_client_secret'
      
      $batchSize = 10
      $delayBetweenBatches = 500  # milliseconds
      $dryRun = $true  # Set to $false to actually update
      
      $authUrl = "https://$tenant.halopsa.com/auth/token"
      $apiUrl = "https://$tenant.halopsa.com/api"
      
      # ============================================
      # LOGGING
      # ============================================
      $timestamp = Get-Date -Format 'yyyyMMdd_HHmmss'
      $logFile = "BulkItemUpdate_$timestamp.log"
      
      function Write-Log {
          param(
              [Parameter(Mandatory=$false)]
              [AllowEmptyString()]
              [string]$Message = '',
              
              [Parameter(Mandatory=$false)]
              [ValidateSet('Black','DarkBlue','DarkGreen','DarkCyan','DarkRed','DarkMagenta','DarkYellow','Gray','DarkGray','Blue','Green','Cyan','Red','Magenta','Yellow','White')]
              [string]$Color = 'White'
          )
          
          if ($Message -eq '') {
              Write-Host ''
              Add-Content -Path $logFile -Value ''
          } else {
              $logMessage = "$(Get-Date -Format 'HH:mm:ss') $Message"
              Write-Host $logMessage -ForegroundColor $Color
              Add-Content -Path $logFile -Value $logMessage
          }
      }
      
      Write-Log -Message "=== HaloPSA Bulk Item Update - Started ===" -Color Cyan
      Write-Log -Message "Log file: $logFile" -Color Cyan
      Write-Log
      
      # ============================================
      # AUTHENTICATE
      # ============================================
      Write-Log -Message 'Authenticating...' -Color Cyan
      try {
          $tokenBody = @{
              grant_type = 'client_credentials'
              client_id = $clientId
              client_secret = $clientSecret
              scope = 'edit:items read:items'
          }
          
          $token = Invoke-RestMethod -Uri $authUrl -Method Post -Body $tokenBody -ErrorAction Stop
          $headers = @{
              Authorization = "Bearer $($token.access_token)"
              'Content-Type' = 'application/json'
          }
          Write-Log -Message '✓ Authentication successful' -Color Green
          Write-Log
      } catch {
          Write-Log -Message '✗ Authentication failed' -Color Red
          Write-Log -Message "Error: $($_.Exception.Message)" -Color Red
          exit 1
      }
      
      # ============================================
      # RETRIEVE ALL ITEMS
      # ============================================
      Write-Log -Message 'Retrieving items...' -Color Cyan
      $allItems = @()
      $page = 1
      $pageSize = 100
      
      try {
          do {
              $itemsUrl = "$apiUrl/Item?page_size=$pageSize&page_no=$page"
              $response = Invoke-RestMethod -Uri $itemsUrl -Headers $headers -Method Get -ErrorAction Stop
              
              if ($response.items) {
                  $allItems += $response.items
              }
              
              $page++
              Start-Sleep -Milliseconds 200
              
          } while ($response.items -and $response.items.Count -eq $pageSize)
          
          Write-Log -Message "✓ Retrieved $($allItems.Count) total items" -Color Green
          Write-Log
      } catch {
          Write-Log -Message '✗ Failed to retrieve items' -Color Red
          Write-Log -Message "Error: $($_.Exception.Message)" -Color Red
          exit 1
      }
      
      # ============================================
      # FILTER ITEMS TO UPDATE
      # ============================================
      Write-Log -Message 'Filtering items...' -Color Cyan
      
      # MODIFY THIS FILTER FOR YOUR USE CASE
      $itemsToUpdate = $allItems | Where-Object { 
          $_.template_id -ne $null -and $_.template_id -ne 0 
      }
      
      if ($itemsToUpdate.Count -eq 0) {
          Write-Log -Message '✓ No items match filter criteria' -Color Green
          exit 0
      }
      
      Write-Log -Message "✓ Found $($itemsToUpdate.Count) items to update" -Color Yellow
      Write-Log
      
      # ============================================
      # PREVIEW
      # ============================================
      Write-Log -Message "Preview - First 10 items:" -Color Cyan
      for ($i = 0; $i -lt [Math]::Min(10, $itemsToUpdate.Count); $i++) {
          $item = $itemsToUpdate[$i]
          Write-Log -Message "  ID: $($item.id) | Name: $($item.name) | template_id: $($item.template_id)"
      }
      Write-Log
      
      # ============================================
      # CONFIRMATION
      # ============================================
      if ($dryRun) {
          Write-Log -Message '*** DRY RUN MODE ***' -Color Magenta
          $confirm = Read-Host 'Continue with dry run? (yes/no)'
      } else {
          Write-Log -Message '*** LIVE MODE - WILL UPDATE ITEMS ***' -Color Red
          $confirm = Read-Host 'Type YES in capitals to confirm'
      }
      
      if ($confirm -ne 'yes' -and $confirm -ne 'YES') {
          Write-Log -Message 'Operation cancelled' -Color Yellow
          exit 0
      }
      
      # ============================================
      # PROCESS UPDATES
      # ============================================
      Write-Log
      Write-Log -Message "Processing updates..." -Color Cyan
      
      $successCount = 0
      $failCount = 0
      $processedCount = 0
      $totalCount = $itemsToUpdate.Count
      
      for ($i = 0; $i -lt $totalCount; $i += $batchSize) {
          $batch = $itemsToUpdate[$i..[Math]::Min($i + $batchSize - 1, $totalCount - 1)]
          
          foreach ($item in $batch) {
              $processedCount++
              $percentComplete = [Math]::Round(($processedCount / $totalCount) * 100, 1)
              
              if ($dryRun) {
                  Write-Log -Message "  [$processedCount/$totalCount ($percentComplete%)] DRY RUN: Would update ID $($item.id)" -Color Gray
                  $successCount++
              } else {
                  try {
                      # MODIFY THIS PAYLOAD FOR YOUR USE CASE
                      $updatePayload = @{
                          id = $item.id
                          template_id = 0
                      }
                      
                      # Manual array wrapper - HaloPSA API requires array
                      $updateBody = "[$($updatePayload | ConvertTo-Json -Depth 5 -Compress)]"
                      
                      $updateResult = Invoke-RestMethod -Uri "$apiUrl/Item" -Headers $headers -Method Post -Body $updateBody -ContentType 'application/json' -ErrorAction Stop
                      
                      Write-Log -Message "  [$processedCount/$totalCount ($percentComplete%)] ✓ Updated ID $($item.id)" -Color Green
                      $successCount++
                      
                  } catch {
                      Write-Log -Message "  [$processedCount/$totalCount ($percentComplete%)] ✗ FAILED ID $($item.id)" -Color Red
                      $failCount++
                  }
                  
                  Start-Sleep -Milliseconds 100
              }
          }
          
          if ($i + $batchSize -lt $totalCount) {
              Start-Sleep -Milliseconds $delayBetweenBatches
          }
      }
      
      # ============================================
      # SUMMARY
      # ============================================
      Write-Log
      Write-Log -Message "=== SUMMARY ===" -Color Cyan
      if ($dryRun) {
          Write-Log -Message "DRY RUN: Items that would be updated: $successCount" -Color Yellow
      } else {
          Write-Log -Message "Successfully updated: $successCount" -Color Green
          Write-Log -Message "Failed: $failCount" -Color $(if ($failCount -gt 0) { 'Red' } else { 'White' })
      }
      Write-Log -Message "Log saved to: $logFile" -Color Cyan
      

      Critical API Requirements

      Array Format: The Items API requires an array payload even for single updates:

      # CORRECT - Array format
      $updateBody = "[{\"id\":363,\"template_id\":0}]"
      
      # WRONG - Single object
      $updateBody = "{\"id\":363,\"template_id\":0}"
      

      PowerShell's ConvertTo-Json strips array wrappers for single elements, so manual wrapping is required:

      $updatePayload = @{ id = 363; template_id = 0 }
      $updateBody = "[$($updatePayload | ConvertTo-Json -Compress)]"
      

      Workflow

      1. Test in dev environment first
      2. Run dry-run - Review log file
      3. Verify preview - Confirm correct items selected
      4. Set $dryRun = $false
      5. Run live update - Type YES to confirm
      6. Verify in UI - Spot-check random items
      7. Deploy to production - After dev verification

      Authentication

      All scripts require HaloPSA API credentials:

      1. ConfigurationIntegrationsHaloPSA APIView Applications
      2. Create application - Select "Services" authentication
      3. Set permissions - read:items, edit:items
      4. Copy Client ID and Client Secret - Secret shown only once
      5. Update script configuration:
        • $tenant = Your HaloPSA subdomain (e.g., 'dtcdev')
        • $clientId = Application Client ID
        • $clientSecret = Application Client Secret

      API Endpoints

      Endpoint Method Purpose
      /Item GET Retrieve items (supports pagination)
      /Item/{id} GET Retrieve single item by ID
      /Item POST Create or update items (array payload)

      Query Parameters

      Parameter Description Example
      page_size Items per page (max 100) ?page_size=50
      page_no Page number ?page_no=2
      includedetails Include all fields ?includedetails=true

      Common Use Cases

      Clear Project Template from All Items

      $itemsToUpdate = $allItems | Where-Object { 
          $_.template_id -ne $null -and $_.template_id -ne 0 
      }
      
      $updatePayload = @{
          id = $item.id
          template_id = 0
      }
      

      Update Supplier for Specific Item Type

      $itemsToUpdate = $allItems | Where-Object { 
          $_.item_type_id -eq 5  # Hardware
      }
      
      $updatePayload = @{
          id = $item.id
          supplier_id = 42
      }
      

      Bulk Price Update

      $itemsToUpdate = $allItems | Where-Object {
          $_.name -like "MNT-*"  # All monitor mounts
      }
      
      $updatePayload = @{
          id = $item.id
          sell_price = $item.sell_price * 1.10  # 10% increase
      }
      

      Troubleshooting

      Error: "Cannot deserialize...into type 'Item[]'"

      Cause: JSON payload is not an array
      Fix: Manually wrap payload in brackets: "[$json]"

      Error: "Cannot bind parameter 'ForegroundColor'"

      Cause: Write-Log function parameter parsing issue
      Fix: Use named parameters: Write-Log -Message "text" -Color Green

      Error: "Authentication failed"

      Check:

      • Client ID and Secret are correct
      • API application has required permissions (read:items, edit:items)
      • Tenant name is correct (no .halopsa.com suffix)
      • API application is active in HaloPSA

      Items not updating despite success message

      Check:

      • Verify in HaloPSA UI
      • Check API response for errors
      • Ensure field name is correct (use field inspector)
      • Verify field type matches payload type

      Best Practices

      1. Always run dry-run first - Preview changes before execution
      2. Test in dev environment - Before production deployment
      3. Use small batch sizes - Avoid API rate limiting
      4. Add delays between batches - 500ms recommended
      5. Log everything - Timestamped logs for audit trail
      6. Verify in UI - Spot-check after bulk updates
      7. Keep backups - Export data before mass modifications