Automation Examples
Return Code Script
This sample script demonstrates how to add a return code to an automation. It is provided for example purposes only and is not part of a released automation.
ReturnCodeSample.ps1
<#
.SYNOPSIS
ExampleAutomationReturnCode.ps1
.DESCRIPTION
This script is used for Advanced Disk Clean that contains multiple user profiles.
.INPUTS
None
.OUTPUTS
All output is written into a log file in the same location from which the script is run.
.NOTES
Version: 1.10.4
Author: Lakeside Software, carsten.giese@lakesidesoftware.com
Requirements: This script assumed to be run by the SysTrack agent (local SYSTEM account). The
folder deletion action requires local administrator permissions.
Changes: 1.0.0 - Initial Release
1.1.0 - Stability improvements, unique error codes, dynamic path discovery
1.2.0 - WMI-based profile deletion, safe junction handling
1.2.1 - Removed dangerous fallback for profiles directory discovery
1.2.2 - Refactored temp cleanup to use array-based approach
1.3.0 - Dynamic drive detection from profiles path using .NET DriveInfo
1.4.0 - Enhanced logging for all deletion operations
1.5.0 - Added diagnostic logging for profile evaluation and skip conditions
1.6.0 - Added check for missing timestamp data to identify orphaned profiles
1.6.1 - Replaced hardcoded orphaned age with boolean flag, improved profile logging
1.6.2 - Changed internal units to MB with explicit variable naming
1.7.0 - Refactored freed space thresholds into configurable hashtable
1.8.0 - Added dynamic unit formatting for freed space display
1.9.0 - Added protected system profile names to prevent deletion
1.9.1 - Fixed logic to allow temp cleanup for all profiles, preserve temp folder structure
1.9.2 - Optimized flow to cleanup first then check deletion, reduced error spam
1.10.0 - Added comprehensive temp cleanup logging for visibility
1.10.1 - Improved variable naming for clarity and safety
1.10.2 - Two-pass bottom-up deletion for better locked-file handling
1.10.3 - Added threshold sorting for robust return code determination
1.10.4 - Further improved variable naming for code clarity
Copyright 2020 LAKESIDE SOFTWARE, INC.
.LINK
http://www.lakesidesoftware.com
#>
# Clean the output screen and reset all cached variables
cls
Remove-Variable * -ea 0
# Set the default for what we want to happen if an unexpected error occurs
# https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_preference_variables?view=powershell-7.3#erroractionpreference
$ErrorActionPreference = "Stop"
# Initialize script-level variables
$script:resetLog = $true
$scriptPath = $null
$logfile = $null
# Logging function with error-safe file operations
function log($text) {
$line = ''
if ($text) {
$time = [datetime]::Now.ToUniversalTime().tostring('s')
$line = [string]::Join(' - ', $time, $text)
}
# Console output if available
if ($psISE -or -not [Console]::IsOutputRedirected) {
Write-Output $line
}
# File output with error suppression to prevent logging failures from crashing script
try {
if ($script:resetLog) {
$line | Out-File $logfile -Encoding utf8 -Force -ErrorAction Stop
$script:resetLog = $false
} else {
$line | Out-File $logfile -Encoding utf8 -Append -ErrorAction Stop
}
}
catch {
# Silently continue if logging fails - don't crash the script
}
}
# Format disk size from MB to appropriate unit for display
function Format-DiskSize($sizeInMB) {
if ($sizeInMB -ge 1024) {
return "$([math]::Round($sizeInMB/1024, 2))GB"
} elseif ($sizeInMB -ge 1) {
return "$([math]::Round($sizeInMB, 2))MB"
} elseif ($sizeInMB -ge 0.001) {
return "$([math]::Round($sizeInMB * 1024, 2))KB"
} else {
return "$([math]::Round($sizeInMB * 1024 * 1024, 2))bytes"
}
}
# Main script body
$scriptExitCode = 0
$profileDeletionAgeThresholdInDays = 60
$skipNonADProfiles = $true
# Define system profile names to protect from deletion
$protectedProfileNames = @(
"WsiAccount",
"DefaultAccount",
"WDAGUtilityAccount",
"Administrator",
"Guest",
"Public",
"Default"
)
# Define profile subdirectories to clean for all profiles
$profileSubfoldersToClean = @(
"AppData\Local\Temp",
"AppData\Local\Temporary Internet Files"
)
# Define freed space thresholds and corresponding return codes
$freedSpaceReturnCodes = @(
@{ ThresholdMB = 100; ReturnCode = 101; Description = "Less than 100MB freed" }
@{ ThresholdMB = 500; ReturnCode = 102; Description = "100MB to 500MB freed" }
@{ ThresholdMB = [int]::MaxValue; ReturnCode = 103; Description = "500MB or more freed" }
)
try {
# Determine script location
if ($PSScriptRoot) {
$scriptPath = $MyInvocation.MyCommand.Path
} elseif ($psISE -and $psISE.CurrentFile) {
if ($psISE.CurrentFile.IsSaved -and $psISE.CurrentFile.FullPath) {
$scriptPath = $psISE.CurrentFile.FullPath
} else {
throw "Script is not saved in ISE"
}
} else {
throw "Unable to determine script location"
}
$logfile = $scriptPath + ".log"
# Log script initialization
log "Script starting ..."
log "Powershell Version: $($PSVersionTable.PSVersion)"
log ".NET CLR Version: $([System.Environment]::Version)"
# --- START OF VERSION CHECK ---
$requiredPSMajorVersion = 5
$currentPSMajorVersion = $PSVersionTable.PSVersion.Major
if ($currentPSMajorVersion -lt $requiredPSMajorVersion) {
log "ERROR: Minimum required PowerShell Major version is $($requiredPSMajorVersion)"
$scriptExitCode = 1
throw
}
# Log execution context
$CurrentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent()
log "Running As: $($CurrentUser.Name)"
if (([Security.Principal.WindowsPrincipal]$CurrentUser).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) {
log "Is an Administrator"
} else {
log "ERROR: Script needs to be started as admin or local System."
$scriptExitCode = 2
throw
}
# Check architecture
log "Architecture: $($Env:PROCESSOR_ARCHITECTURE)"
if ($env:PROCESSOR_ARCHITEW6432 -eq "AMD64") {
log "Running 32-bit Powershell on 64-bit Windows"
}
# Discover user profiles directory from registry
log "Discovering user profiles directory from registry"
try {
$profilesKey = Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList" -Name ProfilesDirectory -ErrorAction Stop
$usersPath = $profilesKey.ProfilesDirectory
if (-not $usersPath -or -not (Test-Path $usersPath)) {
throw "ProfilesDirectory registry value is invalid or path does not exist"
}
log "User profiles directory: $usersPath"
}
catch {
log "ERROR: Failed to discover user profiles directory from registry"
$scriptExitCode = 3
throw
}
# Extract drive letter from profiles path
$driveLetter = Split-Path -Qualifier $usersPath
log "Target drive: $driveLetter"
# Get initial disk information
log "Getting initial disk information"
try {
$driveInfo = [System.IO.DriveInfo]::new($driveLetter)
$freeSpace_before_MB = [math]::Round($driveInfo.AvailableFreeSpace/1MB, 2)
log "Disk Free Space before script run: $([math]::Round($freeSpace_before_MB/1024, 2))GB`n"
}
catch {
log "ERROR: Failed to get initial disk information"
$scriptExitCode = 4
throw
}
# Enumerate WMI user profiles
log "Enumerating WMI user profiles"
try {
$wmiProfiles = Get-WMIObject -class Win32_UserProfile -ErrorAction Stop
log "Found $($wmiProfiles.Count) WMI user profiles"
}
catch {
log "ERROR: Failed to enumerate WMI user profiles"
$scriptExitCode = 5
throw
}
# Enumerate registry profile entries
log "Enumerating registry ProfileList entries"
try {
$profileListKey = "HKLM:\Software\Microsoft\Windows NT\CurrentVersion\ProfileList\*"
$registryProfiles = Get-ItemProperty $profileListKey -ErrorAction Stop
log "Found $($registryProfiles.Count) registry profile entries`n"
}
catch {
log "ERROR: Failed to enumerate registry ProfileList entries"
$scriptExitCode = 6
throw
}
# Process each registry profile entry
$currentDateTime = [datetime]::Now
$profilesProcessed = 0
$profilesSkipped = 0
foreach ($profileRegistry in $registryProfiles) {
$profileSID = $profileRegistry.PSChildName
$profilePath = $profileRegistry.ProfileImagePath
# Log profile identification with path if available, otherwise SID
if ($profilePath) {
log "Evaluating profile: $profilePath ($profileSID)"
} else {
log "Evaluating profile SID: $profileSID (no ProfileImagePath)"
}
# Get corresponding WMI profile object
$wmiProfile = $wmiProfiles | Where-Object {$_.SID -eq $profileSID}
# Perform temp cleanup for ALL profiles first
if ($profilePath -and (Test-Path $profilePath -PathType Container -ea 0)) {
foreach ($profileSubfolder in $profileSubfoldersToClean) {
$subfolderPath = Join-Path $profilePath $profileSubfolder
if (Test-Path $subfolderPath -PathType Container) {
log "Attempting to clean folder: $profileSubfolder"
$successCount = 0
$failureCount = 0
try {
# Get all files in subfolder
$filesToDelete = Get-ChildItem -Path $subfolderPath -Recurse -Force -File -ErrorAction SilentlyContinue
# Delete all files first
foreach ($file in $filesToDelete) {
try {
[System.IO.File]::Delete($file.FullName)
$successCount++
}
catch {
$failureCount++
}
}
# Get all directories and sort by path length (deepest first)
$directoriesToDelete = Get-ChildItem -Path $subfolderPath -Recurse -Force -Directory -ErrorAction SilentlyContinue
$directoriesSorted = $directoriesToDelete | Sort-Object { $_.FullName.Length } -Descending
# Delete folders deepest-first
foreach ($directory in $directoriesSorted) {
try {
[System.IO.Directory]::Delete($directory.FullName, $false)
$successCount++
}
catch {
$failureCount++
}
}
log "Cleaned $successCount items, $failureCount items could not be deleted (in use by other processes)"
}
catch {
log "Failed to access subfolder: ..\$profileSubfolder"
}
} else {
log "Subfolder does not exist: ..\$profileSubfolder"
}
}
}
# Now check deletion criteria
# Skip special profiles from deletion
if ($wmiProfile.Special) {
log "Special profile - skipping deletion`n"
$profilesSkipped++
continue
}
# Skip non-AD profiles from deletion if configured
if ($skipNonADProfiles -and !$profileSID.StartsWith('S-1-5-21')) {
log "Non-AD profile - skipping deletion`n"
$profilesSkipped++
continue
}
# Skip protected system profiles from deletion
if ($profilePath) {
$profileFolderName = Split-Path -Leaf $profilePath
if ($protectedProfileNames -contains $profileFolderName) {
log "Protected system profile: $profileFolderName - skipping deletion`n"
$profilesSkipped++
continue
}
}
# Calculate age and check for orphaned status (only now after filters pass)
$isOrphaned = $false
$profileAgeInDays = 0
if ($profileRegistry.State -eq $null) {
# Orphaned profile with no state
$isOrphaned = $true
log "Orphaned profile detected (no State value)"
} elseif ($null -eq $profileRegistry.PSObject.Properties['LocalProfileLoadTimeHigh']) {
# Orphaned profile with missing timestamp data
$isOrphaned = $true
log "Orphaned profile detected (missing load timestamp)"
} else {
# Calculate age from LocalProfileLoadTime
$high = [uint64]$profileRegistry.LocalProfileLoadTimeHigh
$low = [uint64]$profileRegistry.LocalProfileLoadTimeLow
$lastLoadTime = [datetime]::FromFileTime($high -shl 32 -bor $low)
$profileAgeInDays = ($currentDateTime - $lastLoadTime).TotalDays
}
# Skip profiles with unexpected age values from deletion
if (!$isOrphaned -and (!$profileAgeInDays -or $profileAgeInDays -gt 5000)) {
log "Profile has invalid age ($profileAgeInDays days) - skipping deletion`n"
$profilesSkipped++
continue
}
$profilesProcessed++
# Check if profile should be deleted based on age or orphaned status
if ($isOrphaned -or $profileAgeInDays -gt $profileDeletionAgeThresholdInDays) {
# Delete old or orphaned profile
if ($isOrphaned) {
log "Profile marked for deletion (orphaned)"
} else {
log "Profile marked for deletion - Age: $([math]::Round($profileAgeInDays, 2)) days - Exceeds threshold"
}
if (Test-Path $profilePath -PathType Container) {
log "Attempting to remove profile folder and data"
try {
# Using .NET Directory.Delete() to safely handle junctions without following them into different locations
[System.IO.Directory]::Delete($profilePath, $true)
log "Successfully deleted profile folder: $profilePath"
}
catch {
log "Failed to delete profile folder: $profilePath"
}
} else {
log "Profile folder does not exist, will remove WMI object only"
}
# If folder was successfully deleted or didn't exist, remove WMI profile object
if (-not (Test-Path $profilePath -PathType Container)) {
log "Attempting to remove WMI profile object"
try {
$wmiProfile | Remove-WmiObject -ErrorAction Stop
log "Successfully removed WMI profile object`n"
}
catch {
log "Failed to remove WMI profile object"
log $_.Exception.Message
}
} else {
log "Skipping WMI removal - profile folder still exists`n"
}
}
else {
# Profile is below age threshold
log "Active profile - Age: $([math]::Round($profileAgeInDays, 2)) days - Below threshold`n"
}
}
log "Profile processing summary: $profilesProcessed processed, $profilesSkipped skipped`n"
# Get final disk information
log "Getting final disk information"
try {
$driveInfo = [System.IO.DriveInfo]::new($driveLetter)
$freeSpace_after_MB = [math]::Round($driveInfo.AvailableFreeSpace/1MB, 2)
}
catch {
log "ERROR: Failed to get final disk information"
$scriptExitCode = 7
throw
}
$FreedSpace_MB = [math]::Max(0, $freeSpace_after_MB - $freeSpace_before_MB)
log "Disk Free Space after script run: $([math]::Round($freeSpace_after_MB/1024, 2))GB"
log "Total freed space: $(Format-DiskSize $FreedSpace_MB)"
# Determine return code based on freed space thresholds
$sortedThresholds = $freedSpaceReturnCodes | Sort-Object { $_.ThresholdMB }
foreach ($threshold in $sortedThresholds) {
if ($FreedSpace_MB -lt $threshold.ThresholdMB) {
$scriptExitCode = $threshold.ReturnCode
break
}
}
}
catch {
# General exception handler
if ($scriptExitCode -eq 0) {
log "ERROR: Unexpected exception occurred during script execution"
log $error[0].FullyQualifiedErrorId
log $error[0].Exception.ToString()
log $error[0].ScriptStackTrace
$scriptExitCode = 6
}
}
finally {
log "Script complete returning ($scriptExitCode)"
Exit $scriptExitCode
}ReturnCodeSample.bat
@ECHO OFF
PUSHD %~dps0
SET "SCRIPTFILE=ReturnCodeSample.ps1"
IF NOT EXIST "%SCRIPTFILE%" SET "SCRIPTFILE=%~n0.ps1"
:: Execute the PowerShell script:
SET "SYS32=System32"
IF "%PROCESSOR_ARCHITECTURE%" == "x86" SET "SYS32=SysNative"
SET "POWERSHELL=%WINDIR%\%SYS32%\WindowsPowerShell\v1.0\powershell.exe"
"%POWERSHELL%" -NOPROFILE -EXECUTIONPOLICY BYPASS -FILE "%SCRIPTFILE%" %* > "%~nx0.log" 2>&1
SET "EXITCODE=%ERRORLEVEL%"
POPD
EXIT /B %EXITCODE%Script Output

Sample Hello World Script
This is an automation example. The HelloWorldLogs.bat script is not maintained and is not guaranteed to work. Use at your own risk.
@echo off
PowerShell -ExecutionPolicy Bypass -File "HelloWorldLogs.ps1"
EXIT /B %ERRORLEVEL%
HelloWorldLogs.ps1
<#
.SYNOPSIS
HelloWorld.PS1
.DESCRIPTION
This script will pop up a Hello World Window
.INPUTS
None
.OUTPUTS
When enabled, a log file will be created in the same location from which the script is run.
.NOTES
Version: 1.0
Author: Lakeside Software
Requirements: This script is assumed to be run by the SysTrack agent (local SYSTEM account).
This script is NOT maintained and is not guaranteed to work and is meant as an example
USE AT YOUR OWN RISK
Copyright 2023 LAKESIDE SOFTWARE, INC.
.LINK
http://www.lakesidesoftware.com
#>
#####
# Begin Functions
#####
#####
# Function: LogWrite (Logging)
#####
function LogWrite([string]$info)
{
if($script:log -eq $true)
{
# Write to logfile date - message
"$(get-date -format "yyyy-MM-dd HH:mm:ss") - $info" >> $logfile
# Any logged Start or Exit statements also write to host
If ( ($info.contains("Starting Script")) -or ($info.contains("Exiting Script")) ) {
Write-Host $info
}
# Any logged Warning or Error statements also write to host
If ( ($info.contains("Error:")) -or ($info.contains("Warning"))) {
Write-Host " " $info "(See Log For Details)"
}
}
else {
Write-Host $info
}
}
#####
# Function: NewFile (Logging, Location)
#####
function NewFile($data, $loc)
{
If (test-path $loc) {Remove-Item $loc}
"$data" >> $loc
LogWrite "Data saved to $loc"
}
#####
# Function: ExitScript
#####
function ExitScript($msg, $ExitCode)
{
### Cleanup
If ($Silent) {
Write-Progress -Activity "Working..." `
-Completed -Status $msg
}
### Log Exit
LogWrite "$msg ($ExitCode)"
LogWrite "Exiting Script..."
Logwrite ""
#Exit
Exit($ExitCode)
}
#####
# End Functions
#####
#####
# Advanced Variables
#####
#logging
If (!$Logging) {$Logging = $true}
$ErrorActionPreference = "Stop"
$debug = $false
$retain = $false # new log on each run
$ScriptName = & { $myInvocation.ScriptName }
$ScriptPath = Split-Path -parent $ScriptName
$ScriptName = Split-Path $ScriptName -Leaf
$ScriptName = $ScriptName.Replace(".PS1","")
$logfile = "$ScriptPath\$ScriptName" + "Log.txt"
$script:log = $Logging
$Dev = "False"
#####
# Start Script
#####
cls
### Delete existing log files > 1mb
If (test-path $logfile) {
If ($retain -eq $true) {
If ((Get-Item $logfile).length -gt 1mb) {
Remove-Item $logfile
}
}
Else {
Remove-Item $logfile
}
}
#####
# Start Script
#####
### Start Script
LogWrite ""
LogWrite "Starting Script..."
$WhoAmI = [Environment]::UserName
$Is64Bit = [Environment]::Is64BitProcess
LogWrite "Running As: $WhoAmI"
LogWrite "64bit Process: $Is64Bit"
#LogWrite "About to try writing hello world to a file..."
try{
LogWrite "Start Hello World! Invoking Forms."
Add-Type -AssemblyName System.Windows.Forms
LogWrite "Hello World!!"
}
catch{
LogWrite "Unable to popup Hello World prompt due"
LogWrite $error[0].FullyQualifiedErrorId
LogWrite $error[0].Exception.ToString()
LogWrite $error[0].ScriptStackTrace
ExitScript -msg "Script Failed" -code 1
}
#####
# Exit Script
#####
ExitScript -msg "Script Completed" -code 0