How to keep your Workspace ONE UEM Environment Clean – UEM Maintenance Script for Windows 10

Keeping a clean and healthy environment is important when managing a large number of devices in Workspace ONE UEM. Over time duplicate and stale device records, especially for Windows 10, can grow and grow. Without regular maintenance, you could potentially have thousands of records that are no longer valid and will affect the reporting of your deployments. This happens due to normal attrition of devices, re-imaging, and well as device resets. I took an internal script developed by our VMware IT system admins and modified it for external use so that you can easily clean up device records using WS1 REST API. The script is available on Github: Start-UEMMaintenance_Windows.ps1. To download it, click “Raw” then right click and “Save As”.

What the script do?

This script connects to your VMware Workspace ONE UEM environment and gets duplicates, stale records, or problematic devices (devices with invalid serials). Once you run any of the “get” actions, it will save the data to several csv files located under C:\UEM-Maintenance\[uem server]\Win_[today’s date]. If you run it again on the same day, it will search for valid CSV files first before reaching out again to the server. This is to improve speed, allow for editing of CSVs, and reduce load on the server. If it does not find a valid csv, it will go ahead and reach out to the server. Additionally, it asks for and stores the UEM credentials in an encrypted file (saved C:\UEM-Maintenance\ [uem server]\Logs\Creds.txt) with AES encryption. The key is saved C:\UEM-Maintenance\ [uem server]\Logs\AES.key. This allows the script to be run in an automated fashion by a service account or multiple users on the same internal server. However since the encryption key is on the same system, care must be taken on who can access the system this script is running from. This key can also be saved on a different location for improved security.


Using REST API with Workspace One involves two things: first, you need an API key. This essentially “gets you in the door” and is tied to the Organization Group. However, to actually do anything, such as reading or deleting devices you still need a Workspace One admin account. This can either be a basic account or a service account synced in from Active Directory. The minimum this account needs is REST API access as well as “delete’ device records. The simplest way to achieve it is the grant the account “Airwatch Administrator” access. However, this gives it way more access than it needs so best practice always is to create a least privileged access role. If you do set it to “Airwatch Administrator”, give it that role during the clean up and then change the account a “read only” role. This is to ensure no one external ever get the API key and admin credentials.

Additionally, as mentioned above when you run the script the first time (per environment) it will prompt you for those admin credentials and then save them encrypted to C:\UEM-Maintenance\ [uem server]\Logs\Creds.txt. The key to unlock and retrieve the creds is in the same directory so this isn’t a true foolproof encrypted solution. It adds a layer of security but you still need to protect the AES.key file. You can do this several ways: First, delete the file after each time this script is run. If you do this then you will have to input credentials each time you run the script. Second, you can run this on an internal server where only legitimate admins have access. Or you can modify the script to store and retrieve this file from a different fileshare on your network.

With that out of the way, let’s dig into each function and how they work.



This function first gets all Windows 10 devices and saved them to Win_AllDevices.csv. Next, it goes through each record and counts how many times each serial number appears. Then it makes a new list of all of the devices that have more than one occurrence of a serial number and saves it to Win_AllDuplicateDevices.csv. It loops though that list and sorts based on “Device Last Seen” and then by “Device ID” so that most recent device (meaning the “good” device) is excluded. All of these exclusions are sent to Win_DoNotDeleteList.csv. The rest are the true duplicates and should be deleted. This list is Win_DuplicateDevicesToBeDeleted.csv and will be used by the “Delete-Duplicates” function. You can look at these files as well as edit them for accuracy as well as to reduce the final deletion list. I.e. as a test you could delete all rows except one in the Win_DuplicateDevicesToBeDeleted.csv file so that only one device will be deleted. This is a good test to ensure your credentials work properly. Additionally, this can sometimes take a long time depending on how large of an environment it is. I’ve seen it take 45 min for 160k device environment as well as several hours for a 300k+ device environment. Give it enough time to complete.

This function optionally supports -FilterSerial which means it excludes devices with invalid serials as those may not be true duplicates. See below for more details on this.


This will delete all devices found in Win_DuplicateDevicesToBeDeleted.csv. If that file is not there, it will then run Get-Duplicates.


This first looks for an Win_AllDevices.csv file and will import it if found. Otherwise it will run the “Get-Duplicates” command and pull live from the server. Then after all devices are gathered, it will then sort and look for all devices not seen in the last 90 days (default). If you specify a different value in the “day” parameter, then you can adjust how far back you want to filter. After this it will save all stale devices to Win_StaleDevices.csv.


This will delete all stale devices found in Win_StaleDevices.csv. If that file is not there, it will then run Get-Stale.


This first looks for an Win_AllDevices.csv file and will import it if found. Otherwise it will run the “Get-Duplicates” command and pull live from the server. Then after all devices are gathered, it will then sort based on the $SerialFilter variable at the top of the script. At the time of this writing, I have it set to filter any of these values

'System Serial Number',
'To be filled by O.E.M.',
'Default string',

Devices can have invalid serials for a number of reasons and need to be looked at why the inventory is not working properly. This can sometimes indicate a bad or incomplete enrollment. This function will save all problematic devices found to Win_ProblematicDevices.csv


This will delete all problematic devices found in Win_ProblematicDevices.csv. If that file is not found, it will run Get-Problematic.

Optional Parameters

Specifies the server you are targeting in format

Mandatory parameter for the API key that is requred for the script to connect via REST API to your server. These keys are per OG and are found under All Settings > System > Advanced > API > REST API.

Mandatory parameter that specifies the action the script should take. Options are: ‘Get-Duplicates’, ‘Delete-Duplicates’, ‘Get-Stale’, ‘Delete-Stale’, ‘Get-Problematic’, ‘Delete-Problematic’.

Optional parameter that specifies how many days back the script should check for stale records. Default is 90 days.

Optional parameter that filters out serials that are improperly formatted or not yet populated. See the $SerialFilter variable below for the full list. Use this for duplicates only and not stale or problematic.


Get Duplicates, Filtering Serials
.\Start-UEMMaintenance_Windows.ps1 -server -ApiKey “zwhD99G6593LDO0D93A030139nZti0sur0Gg=” -Action Get-Duplicates -FilterSerial

Get Stale devices (default of 90 days)
.\Start-UEMMaintenance_Windows.ps1 -server -ApiKey “zwhD99G6593LDO0D93A030139nZti0sur0Gg=” -Action Get-Stale

Get Stale devices older than 120 days
.\Start-UEMMaintenance_Windows.ps1 -server -ApiKey “zwhD99G6593LDO0D93A030139nZti0sur0Gg=” -Action Get-Stale -Days 120

Get problematic devices
.\Start-UEMMaintenance_Windows.ps1 -server -ApiKey “zwhD99G6593LDO0D93A030139nZti0sur0Gg=” -Action Get-Problematic

Delete Duplicates
.\Start-UEMMaintenance_Windows.ps1 -server -ApiKey “zwhD99G6593LDO0D93A030139nZti0sur0Gg=” -Action Delete-Duplicates -FilterSerial

Delete Stale
.\Start-UEMMaintenance_Windows.ps1 -server -ApiKey “zwhD99G6593LDO0D93A030139nZti0sur0Gg=” -Action Delete-Stale

Delete Problematic
.\Start-UEMMaintenance_Windows.ps1 -server -ApiKey “zwhD99G6593LDO0D93A030139nZti0sur0Gg=” -Action Delete-Problematic

And that’s the script! I would love to hear feedback on how well this works in your environment as well as things I should add to improve it.

This Post Has 9 Comments

  1. maxime Crouzet

    Hi Brooks,

    an alternative solution to improve security, instead of storing credentials in an encrypted file, is using API/system/admins/session. It provide a temp tenant key and an access token. That way, username and password will never be stocked anywhere.

    It is available starting from Workspace ONE UEM 1907 release (waiting for Oauth authN which should be released with 2001).

    best regards,

  2. Rafael Valdovinos


    I have some WIN10 devices that are not seen for 1 day in my WS1 Console (prepared specially for testing this script). When I run the script to get the stale devices with parameter “-Days 1” the file Win_StaleDevices.csv is created and populated with the correct devices (devices that do not communicate with UEM for 1 day) . After that I´ve tried to leave only one device in the Win_StaleDevices.csv file and execute the script with option “-Action Delete-Stale” to check if I the user has proper delete permissions but the scrit fails. This is the output:

    “Start Log
    Getting encrypted credentials…
    Checking if export folder C:\UEM-Maintenance\\Win_2020-05-06 exists.
    Folder C:\UEM-Maintenance\\Win_2020-05-06 exists
    Looking for existing csv of stale devices…
    C:\UEM-Maintenance\\Win_2020-05-06\Win_StaleDevices.csv found!
    Importing Win_StaleDevices.csv
    1 devices found.
    0 devices have not communicated with UEM since “2020-05-05 (90 days)”.
    0 stale devices exported to “C:\UEM-Maintenance\\Win_2020-05-06\Win_StaleDevices.csv”.
    Inputdata parameter is null in C:\Temp\Script\Start-UEMMaintenance_Windows.ps1: 546 Carácter: 51
    + … tions = Format-DevicesForDelete -InputData ($global:StaleToBeDeleted)
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : InvalidData: (:) [Format-DevicesForDelete], ParameterBindingValidationException
    + FullyQualifiedErrorId : ParameterArgumentValidationErrorNullNotAllowed,Format-DevicesForDelete

    Delete-DevicesFromUEM : No se puede enlazar el argumento al parámetro ‘InputData’ porque es nulo.
    En C:\Temp\OtroScript\Start-UEMMaintenance_Windows.ps1: 547 Carácter: 36
    + Delete-DevicesFromUEM -InputData $deletions
    + ~~~~~~~~~~
    + CategoryInfo : InvalidData: (:) [Delete-DevicesFromUEM], ParameterBindingValidationException
    + FullyQualifiedErrorId : ParameterArgumentValidationErrorNullNotAllowed,Delete-DevicesFromUEM”

    I´ve also tried to set the $days parameter in the script to “1” but it is still not working.

    Does your script should work the way I´m trying or it is not possible to delete devices not seen for 1 day? Am I doing something wrong?

    Thanks for your script and for your time.

  3. Rafael Valdovinos


    I´ve been further ivestigating the problem I mentioned yesterday about the Delete Stale function. I´ve found out that everytime I open the Win_StaleDevices.csv file and delete some records corresponding to devices I don´t really want to delete in spite of corresponding to Stale machines the execution crushes with the message I showed you yesterday.
    If i leave the Win_StaleDevices.csv untouched everythig works perfectly. I´ve tried with 1, 3 or 8 days as stale period and everytime it has work. I don´t know if the Win_StaleDevices.csv file can really be edited.
    A great feature for your script would be the possibility of reducing the scope of searchs/actions to specific organization groups.
    Again, ank you for your script and your time

    1. Brooks Peppin

      Hi Rafael,
      I can take another look at the script to see what’s going on. Are you deleting entire rows?


  4. Jeremy

    I have tried running this on multiple Workspace One instances using the Get-Duplicates and Get-Stale commands but both yield zero devices when ran. I verified the API key and it appears to be correct and working as if I put in an invalid one I get an error as expected. Looking at the logs it creates all the files and behaves as it should other than yielding zero results. Any ideas?

    1. Brooks Peppin

      Hi Jeremy – What version of the console are you on? There is a bug in 2003 that can prevent data from getting properly returned due to a data error on the database side. It is supposedly getting fixed in 2006. If you look at line 475 of the script, you will see that the pagesize is set to 30,000 and will loop until there are no more devices. You can change that to 500, 1000, etc to see if you get data back.

  5. Lakshmi Srinivas Kommina

    First, thanks for the great work Brooks. Just tried this script on a 10000+ device environment and successfully deleted 87 duplicate devices. Added additional criteria for $SerialFilter and that worked well too. Just a bit of feedback on trying out “Delete-Duplicates” function.

    Quoiting this from the write up. “You can look at these files as well as edit them for accuracy as well as to reduce the final deletion list. I.e. as a test you could delete all rows except one in the Win_DuplicateDevicesToBeDeleted.csv file so that only one device will be deleted.”

    The script seems to always referring to “Win_AllDuplicateDevices.csv” and regenerating “Win_DoNotDeleteList.csv” and “Win_DuplicateDevicesToBeDeleted.csv” on every run. So, any changes to “Win_DuplicateDevicesToBeDeleted.csv” for testing purposes or to exclude some devices doesn’t work. So, when I tried to just keep one device in the file and tried to run Delete-Duplicates, all the duplicates are deleted. No harm done as I have already checked the list and built custom $SerialFilter.

    Perhaps, the modifications should be done on “Win_AllDuplicateDevices.csv” instead for limited testing? I haven’t tried it. But that should work based on the script output I am seeing.

  6. Dmitri

    Like Brooks above, returns 0 results. This looks like it would be ideal for us, but so far not working 🙁 Any additional idea why that may be?

  7. Dmitri

    EDIT: Didn’t realize this was limited to Windows devices, should have clued in based on name. Removing platform=WinRT& from Line 475 gave me what I needed! Thanks

Leave a Reply