[Feature Request]: Remove Default Locations of Multiple Assets at Once
Is your feature request related to a problem? Please describe.
Long ago, I imported assets in with default locations via CSV. After checking them in, they still show up in the location because they are assigned to the default location. I can only remove the default locations one-by-one. If I select all assets > edit,, I am not given an option to remove the default location like I would when I am editing a single asset.
Describe the solution you'd like
The ability to remove the default location for several assets at once.
Describe alternatives you've considered
No response
Additional context
Editing Multiple Assets:

Editing a Single Asset:

I think this was resolved quite some time ago
I don't think this is resolved, I'm running into the same issue. I want to mass remove both actual and default locations from assets, but in the bulk editor you can only update locations, not remove them.
I run into this problem quite frequently. Especially when archiving assets the locations remains and can't be deleted.
As a workaround I'm using this PowerShell script to remove the location and default location from all archived assets.
It's AI generated and far from perfect, but get's the work done.
Adjust it to your needs.
<#
.SYNOPSIS
Clears location_id and rtd_location_id only for archived assets that have one of these fields set.
Robust paging using limit & offset with duplicate-detection to avoid infinite loops.
.NOTES
- Uses limit & offset (not page/per_page).
- If the API returns 'total' / 'Total' we use that to stop early.
- If no total is present we stop when a page returns no *new* asset IDs (duplicate detection).
- Also has a MaxLoops fail-safe.
.PARAMETER BaseUrl
Base URL for the Snipe-IT API (e.g. https://inventory.example.com/api/v1)
.PARAMETER Token
API token (defaults to $env:INVENTAR_API_TOKEN)
.PARAMETER Limit
Number of items to request per call (default 50). Snipe-IT may cap this (default 500 server-side as of some versions).
.PARAMETER Execute
If supplied, PATCH requests will actually be executed. Without -Execute the script runs in DryRun mode.
.EXAMPLE
# Dry run
pwsh .\Clear-SnipeArchivedLocations.ps1 -BaseUrl "https://inventory.example.com/api/v1"
# Execute changes
pwsh .\Clear-SnipeArchivedLocations.ps1 -BaseUrl "https://inventory.example.com/api/v1" -Execute
#>
param(
[string]$BaseUrl = "https://inventory.example.com/api/v1",
[string]$Token = $env:INVENTAR_API_TOKEN,
[int]$Limit = 50,
[switch]$Execute
)
if (-not $Token) {
Write-Error "No API token found. Set INVENTAR_API_TOKEN or pass -Token."
exit 1
}
$Headers = @{
"Authorization" = "Bearer $Token"
"Accept" = "application/json"
"Content-Type" = "application/json"
}
function Get-ArchivedHardware_Paged {
param(
[string]$ApiBase,
[int]$Limit
)
$offset = 0
$all = @()
$seenIds = @{} # hashtable for seen IDs
$MaxLoops = 100 # failsafe upper bound
$loop = 0
while ($true) {
$loop++
if ($loop -gt $MaxLoops) {
Write-Warning "Reached max loop count ($MaxLoops). Stopping to avoid infinite loop."
break
}
# build URI with limit & offset; include status=Archived
$uri = "{0}/hardware?status=Archived&limit={1}&offset={2}&sort=created_at&order=desc" -f $ApiBase.TrimEnd('/'), $Limit, $offset
try {
# Use Invoke-RestMethod so JSON -> PSObject
$resp = Invoke-RestMethod -Method Get -Uri $uri -Headers $Headers -ErrorAction Stop
}
catch {
Write-Error ("Error fetching offset {0}: {1}" -f $offset, $_.Exception.Message)
break
}
# Try to extract list rows in a few different known shapes
if ($null -ne $resp.rows) {
$rows = $resp.rows
}
elseif ($null -ne $resp.Rows) {
$rows = $resp.Rows
}
elseif ($null -ne $resp.payload) {
$rows = $resp.payload
}
elseif ($null -ne $resp.data) {
$rows = $resp.data
}
elseif ($resp -is [System.Array]) {
$rows = $resp
}
else {
# fallback: maybe resp contains a single object - wrap it
$rows = @()
}
# Try to get total if provided by the API
$total = $null
if ($resp.PSObject.Properties.Match('total').Count -gt 0) { $total = $resp.total }
elseif ($resp.PSObject.Properties.Match('Total').Count -gt 0) { $total = $resp.Total }
elseif ($resp.PSObject.Properties.Match('count').Count -gt 0) { $total = $resp.count }
elseif ($resp.PSObject.Properties.Match('Count').Count -gt 0) { $total = $resp.Count }
# Logging
Write-Host ("Fetched offset {0} (limit {1}) => returned {2} items. Total reported: {3}" -f $offset, $Limit, ($rows.Count), ($total -ne $null ? $total : "n/a"))
# If the API tells us the total and we've passed it, stop
if ($total -ne $null -and $offset -ge [int]$total) {
Write-Host "Offset >= reported total. Stopping."
break
}
# If no rows at all -> break
if (-not $rows -or $rows.Count -eq 0) {
Write-Host "No rows returned for this offset. Stopping."
break
}
# Add only new (not seen) items to $all and detect duplicates
$newFound = 0
foreach ($r in $rows) {
# robust ID extraction
$id = $null
if ($r.PSObject.Properties.Match('id').Count -gt 0) { $id = $r.id }
elseif ($r.PSObject.Properties.Match('ID').Count -gt 0) { $id = $r.ID }
if ($null -eq $id) { continue }
if (-not $seenIds.ContainsKey($id)) {
$seenIds[$id] = $true
$all += $r
$newFound++
}
else {
# already seen this id
}
}
if ($newFound -eq 0) {
# No new IDs in this page -> likely we've looped or API returned duplicates -> stop.
Write-Host "No new unique items in this page (all IDs already seen). Stopping to avoid infinite loop."
break
}
# If response provided a total and we've now collected >= total, stop
if ($total -ne $null -and $seenIds.Count -ge [int]$total) {
Write-Host "Collected items >= reported total. Stopping."
break
}
# prepare for next page
$offset += $Limit
# small delay optional (rate-limit safety)
Start-Sleep -Milliseconds 200
}
return $all
}
function LocationNotEmpty {
param($asset)
$locId = $null
$rtdId = $null
if ($asset.PSObject.Properties.Match('location_id').Count -gt 0) { $locId = $asset.location_id }
elseif ($asset.PSObject.Properties.Match('location').Count -gt 0) {
if ($asset.location -is [System.Management.Automation.PSObject] -and $asset.location.id) {
$locId = $asset.location.id
}
else {
$locId = $asset.location
}
}
if ($asset.PSObject.Properties.Match('rtd_location_id').Count -gt 0) { $rtdId = $asset.rtd_location_id }
elseif ($asset.PSObject.Properties.Match('rtd_location').Count -gt 0) {
if ($asset.rtd_location -is [System.Management.Automation.PSObject] -and $asset.rtd_location.id) {
$rtdId = $asset.rtd_location.id
}
else {
$rtdId = $asset.rtd_location
}
}
$isLocSet = ($locId -ne $null) -and ($locId -ne 0) -and ($locId -ne "")
$isRtdSet = ($rtdId -ne $null) -and ($rtdId -ne 0) -and ($rtdId -ne "")
return ($isLocSet -or $isRtdSet)
}
function PatchClearLocations {
param(
[Parameter(Mandatory = $true)][int]$Id,
[Parameter(Mandatory = $true)][string]$ApiBase,
[switch]$DoIt
)
$uri = "{0}/hardware/{1}" -f $ApiBase.TrimEnd('/'), $Id
$bodyObj = [PSCustomObject]@{
location_id = $null
rtd_location_id = $null
}
$json = $bodyObj | ConvertTo-Json -Depth 5
if ($DoIt) {
try {
$result = Invoke-RestMethod -Method Patch -Uri $uri -Headers $Headers -Body $json -ErrorAction Stop
Write-Host ("Patched Asset {0}: OK" -f $Id)
return $true
}
catch {
Write-Warning ("Error patching Asset {0}: {1}" -f $Id, $_.Exception.Message)
return $false
}
}
else {
Write-Host ("[DryRun] Would PATCH {0} with body: {1}" -f $uri, $json)
return $null
}
}
# --- Flow ---
Write-Host "Fetching archived assets (limit/offset paging) from $BaseUrl ..."
$archivedAssets = Get-ArchivedHardware_Paged -ApiBase $BaseUrl -Limit $Limit
if (-not $archivedAssets -or $archivedAssets.Count -eq 0) {
Write-Warning "No archived assets found."
exit 0
}
Write-Host ("Total archived assets loaded (unique): {0}" -f $archivedAssets.Count)
$targets = $archivedAssets | Where-Object { LocationNotEmpty $_ }
Write-Host ("Archived assets with set location/rtd_location: {0}" -f $targets.Count)
if ($targets.Count -eq 0) {
Write-Host "No assets found that need cleaning. Exiting."
exit 0
}
$summary = [PSCustomObject]@{
TotalToPatch = $targets.Count
Success = 0
Failed = 0
DryRun = (-not $Execute)
}
foreach ($a in $targets) {
$id = $a.id
if (-not $id) { $id = $a.ID }
if (-not $id) {
Write-Warning "Asset without ID skipped: $($a | ConvertTo-Json -Depth 2)"
$summary.Failed++
continue
}
$tag = $a.asset_tag
$name = $a.name
Write-Host ("Processing Asset ID {0} (Tag: {1}, Name: {2})" -f $id, $tag, $name)
$res = PatchClearLocations -Id $id -ApiBase $BaseUrl -DoIt:$Execute
if ($res -eq $true) { $summary.Success++ } elseif ($res -eq $false) { $summary.Failed++ }
}
Write-Host "Done. Summary:"
$summary | Format-List
if (-not $Execute) {
Write-Host ""
Write-Host "Note: Ran in DryRun mode. To actually apply the changes, run the script with -Execute."
}