Use these sample scripts as a starting point for your own automations. They show how to return a code from an automation, how to start 64-bit PowerShell from the 32-bit Windows agent, and how to keep a process running after the automation ends.
Return Code Script
This sample script shows how to add a return code to an automation. It is provided for example purposes only and is not part of a released automation.
The sample has two files: ReturnCodeSample.ps1 and the launcher ReturnCodeSample.bat. To use them in an automation, put both files in the Action and select ReturnCodeSample.bat as File To Execute. When the automation runs as System, the agent starts the batch file in the folder of the Action.
ReturnCodeSample.ps1
WARNING: This sample deletes data. It deletes temporary files in every user profile, and deletes domain user profiles that were not loaded in the last 60 days. Test it on a test system before you use it.
<#
.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
ReturnCodeSample.bat starts ReturnCodeSample.ps1 in 64-bit PowerShell. On 64-bit Windows, the Windows agent runs as a 32-bit process, and a 32-bit process that starts PowerShell from the System32 folder gets 32-bit PowerShell. In a 32-bit process on 64-bit Windows, PROCESSOR_ARCHITECTURE is x86, so the batch file starts PowerShell from the SysNative folder, which gives 64-bit PowerShell. In all other cases, it uses System32. The batch file passes its arguments to the script, writes the script output to a log file next to it, and returns the exit code of the script.
@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
The following image shows the output of ReturnCodeSample.ps1.

Keep a Process Running After the Automation Ends
On Windows, any process that an automation starts, for example from a batch file, is ended when the action ends or times out. This is a safety mechanism, so that a runtime limit ends everything that the action started.
If a process must continue running after the action ends, start it with the following PowerShell command. The process is started outside the action, so it is not ended with it.
WARNING: If you run a process detached from its parent process, the SysTrack agent can't close it. Thoroughly test any action that you create with this method.
Invoke-WmiMethod -Class Win32_Process -Name Create -ArgumentList ExePathThe following example is trimmed from a sample automation that stops Outlook and then starts it again with this method, so that Outlook keeps running after the automation ends.
$outlookEXEPath = $null
try{
$retCode = 0
$outlookInitialInstance = Get-Process outlook -ErrorAction SilentlyContinue
if ($outlookInitialInstance -ne $null) {
$outlookEXEPath = $outlookInitialInstance.Path
Stop-Process -name outlook -ErrorAction SilentlyContinue
}
# Wait for a bit to make sure Outlook is fully shutdown.
start-sleep -seconds 30
# [Start of your script body]
# [End of your script body]
}
catch{
$retCode = 1
}
finally{
# If we had a path, we tried to shutdown Outlook. So before we leave, try to restart it now.
if ($outlookEXEPath -ne $null) {
$process = Invoke-WmiMethod -Class Win32_Process -Name Create -ArgumentList $outlookEXEPath
# If it did not appear to start, then return a different error code (of your choosing).
If ($process.ReturnValue -ne 0)
{
$retCode = 1000
}
}
Exit($retCode)
}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