Overview
NinjaOne's scripting capabilities enable powerful automation across your managed endpoints. This guide covers building a comprehensive PowerShell script library that leverages NinjaOne's custom fields, scheduled tasks, and condition-based automation to streamline IT operations.
Who Should Use This Guide:
- MSP technicians automating client management
- IT administrators building endpoint automation
- NinjaOne administrators setting up scripting workflows
- DevOps engineers integrating RMM with automation pipelines
NinjaOne Scripting Capabilities:
| Feature | Description |
|---|---|
| Script Library | Centralized repository for reusable scripts |
| Custom Fields | Store script output for reporting/alerts |
| Scheduled Scripts | Time-based automation (maintenance windows) |
| Condition Scripts | Trigger scripts based on device conditions |
| Script Variables | Pass dynamic values to scripts |
| Run As | Execute as SYSTEM, logged-in user, or specific account |
Requirements
NinjaOne Requirements:
| Component | Requirement |
|---|---|
| Account type | Administrator or with scripting permissions |
| Agent version | Latest NinjaOne agent (auto-updates) |
| Endpoints | Windows 10/11, Windows Server 2016+ |
| Script timeout | Default 60 minutes (configurable) |
PowerShell Requirements:
| Platform | Version |
|---|---|
| Windows 10/11 | PowerShell 5.1 (built-in) |
| Windows Server | PowerShell 5.1+ |
| Cross-platform | PowerShell 7+ (optional) |
Architecture
┌─────────────────────────────────────────────────────────────────┐
│ NinjaOne Scripting Architecture │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ │
│ │ Script Library │ │
│ │ ┌───────────┐ │ ┌────────────────────────────────────┐│
│ │ │Remediation│ │ │ Execution Triggers ││
│ │ ├───────────┤ │ │ ┌──────────┐ ┌─────────┐ ┌───────┐ ││
│ │ │Maintenance│ │────▶│ │ Scheduled│ │Condition│ │ Manual│ ││
│ │ ├───────────┤ │ │ │ Task │ │ Script │ │ Run │ ││
│ │ │ Inventory │ │ │ └────┬─────┘ └────┬────┘ └───┬───┘ ││
│ │ ├───────────┤ │ └──────┼────────────┼──────────┼─────┘│
│ │ │ Alerts │ │ │ │ │ │
│ │ └───────────┘ │ ▼ ▼ ▼ │
│ └─────────────────┘ ┌─────────────────────────────────┐ │
│ │ NinjaOne Agent │ │
│ ┌─────────────────┐ │ ┌───────────────────────────┐ │ │
│ │ Custom Fields │◀────│ │ PowerShell Execution │ │ │
│ │ ┌───────────┐ │ │ │ (SYSTEM/User Context) │ │ │
│ │ │ Text │ │ │ └───────────────────────────┘ │ │
│ │ │ Number │ │ │ │ │ │
│ │ │ Dropdown │ │ │ ▼ │ │
│ │ │ Checkbox │ │ │ ┌───────────────────────────┐ │ │
│ │ │ Date │ │ │ │ Script Output/Logs │ │ │
│ │ └───────────┘ │ │ └───────────────────────────┘ │ │
│ └─────────────────┘ └─────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘Process
Step 1: Configure Custom Fields
Set up custom fields to store script output and enable reporting.
Navigate to: Administration → Devices → Global Custom Fields
Create Essential Custom Fields:
| Field Name | Type | Description |
|---|---|---|
| Last_Script_Run | Date/Time | Timestamp of last script execution |
| Disk_Space_GB | Decimal | Available disk space |
| Pending_Updates | Number | Count of pending Windows updates |
| BitLocker_Status | Dropdown | Enabled/Disabled/NotSupported |
| Last_Reboot_Days | Number | Days since last reboot |
| Backup_Status | Dropdown | Success/Failed/NoBackup |
| Software_Inventory | Multi-line | Installed software list |
| Compliance_Score | Number | Custom compliance percentage |
Create Custom Field via UI:
- Click + Add
- Configure:
- Label:
Last Reboot Days - Name:
lastRebootDays - Type: Number
- Scope: Device (or Organization/Location)
- Technician Permission: Read Only
- Label:
- Click Save
Access Custom Fields in Scripts:
# Write to custom field (using NinjaOne cmdlets)
Ninja-Property-Set lastRebootDays $daysSinceReboot
# Write text field
Ninja-Property-Set softwareInventory $softwareList
# Write dropdown (must match defined options)
Ninja-Property-Set bitlockerStatus "Enabled"Step 2: Create Script Library Structure
Organize scripts into logical categories for easy management.
Recommended Folder Structure:
Script Library/
├── Inventory/
│ ├── Get-SystemInfo.ps1
│ ├── Get-InstalledSoftware.ps1
│ ├── Get-UserProfiles.ps1
│ └── Get-NetworkConfiguration.ps1
├── Maintenance/
│ ├── Clear-TempFiles.ps1
│ ├── Optimize-Disk.ps1
│ ├── Update-WindowsApps.ps1
│ └── Restart-Services.ps1
├── Security/
│ ├── Get-BitLockerStatus.ps1
│ ├── Check-AntivirusStatus.ps1
│ ├── Get-LocalAdmins.ps1
│ └── Set-SecurityBaseline.ps1
├── Remediation/
│ ├── Fix-PrintSpooler.ps1
│ ├── Repair-WMI.ps1
│ ├── Clear-BrowserCache.ps1
│ └── Reset-NetworkStack.ps1
└── Monitoring/
├── Check-ServiceStatus.ps1
├── Check-DiskSpace.ps1
├── Check-PendingReboot.ps1
└── Check-CertificateExpiry.ps1Create Script in NinjaOne:
- Navigate to Administration → Library → Scripting
- Click + Add → New Script
- Configure:
- Name: Descriptive name with category prefix
- Description: Detailed purpose and parameters
- Categories: Add relevant tags
- OS: Windows
- Architecture: All / 64-bit / 32-bit
- Run As: SYSTEM (most common)
Step 3: Build Core Automation Scripts
Create reusable scripts for common automation tasks.
Script 1: System Information Collection
<#
.SYNOPSIS
Collects comprehensive system information and updates NinjaOne custom fields.
.DESCRIPTION
Gathers OS info, hardware specs, network config, and updates relevant custom fields.
.NOTES
Schedule: Daily
Run As: SYSTEM
#>
# Get system information
$os = Get-CimInstance -ClassName Win32_OperatingSystem
$computer = Get-CimInstance -ClassName Win32_ComputerSystem
$bios = Get-CimInstance -ClassName Win32_BIOS
# Calculate uptime and days since reboot
$lastBoot = $os.LastBootUpTime
$uptime = (Get-Date) - $lastBoot
$daysSinceReboot = [math]::Round($uptime.TotalDays, 1)
# Get disk space (C: drive)
$disk = Get-CimInstance -ClassName Win32_LogicalDisk -Filter "DeviceID='C:'"
$freeSpaceGB = [math]::Round($disk.FreeSpace / 1GB, 2)
$totalSpaceGB = [math]::Round($disk.Size / 1GB, 2)
$usedPercent = [math]::Round((($totalSpaceGB - $freeSpaceGB) / $totalSpaceGB) * 100, 1)
# Get memory info
$totalRAM = [math]::Round($computer.TotalPhysicalMemory / 1GB, 2)
$freeRAM = [math]::Round((Get-CimInstance -ClassName Win32_OperatingSystem).FreePhysicalMemory / 1MB, 2)
# Update NinjaOne custom fields
Ninja-Property-Set lastRebootDays $daysSinceReboot
Ninja-Property-Set diskSpaceGB $freeSpaceGB
Ninja-Property-Set diskUsedPercent $usedPercent
# Output for logging
Write-Output "=== System Information ==="
Write-Output "Computer: $($computer.Name)"
Write-Output "OS: $($os.Caption) $($os.Version)"
Write-Output "Last Boot: $lastBoot"
Write-Output "Uptime: $($uptime.Days) days, $($uptime.Hours) hours"
Write-Output "Disk Free: $freeSpaceGB GB ($usedPercent% used)"
Write-Output "RAM: $totalRAM GB total, $freeRAM GB free"Script 2: Pending Updates Check
<#
.SYNOPSIS
Checks for pending Windows updates and updates custom fields.
.DESCRIPTION
Uses Windows Update API to get pending update count and details.
.NOTES
Schedule: Daily
Run As: SYSTEM
#>
try {
# Create Windows Update Session
$updateSession = New-Object -ComObject Microsoft.Update.Session
$updateSearcher = $updateSession.CreateUpdateSearcher()
# Search for pending updates
$searchResult = $updateSearcher.Search("IsInstalled=0 AND IsHidden=0")
$pendingCount = $searchResult.Updates.Count
# Categorize updates
$criticalCount = 0
$securityCount = 0
$otherCount = 0
foreach ($update in $searchResult.Updates) {
$categories = $update.Categories | ForEach-Object { $_.Name }
if ($categories -contains "Critical Updates") {
$criticalCount++
} elseif ($categories -contains "Security Updates") {
$securityCount++
} else {
$otherCount++
}
}
# Update NinjaOne custom fields
Ninja-Property-Set pendingUpdates $pendingCount
Ninja-Property-Set criticalUpdates $criticalCount
Ninja-Property-Set securityUpdates $securityCount
# Determine update status
if ($criticalCount -gt 0) {
Ninja-Property-Set updateStatus "Critical"
} elseif ($securityCount -gt 0) {
Ninja-Property-Set updateStatus "Security"
} elseif ($pendingCount -gt 0) {
Ninja-Property-Set updateStatus "Updates Available"
} else {
Ninja-Property-Set updateStatus "Up to Date"
}
# Output summary
Write-Output "=== Windows Update Status ==="
Write-Output "Pending Updates: $pendingCount"
Write-Output " Critical: $criticalCount"
Write-Output " Security: $securityCount"
Write-Output " Other: $otherCount"
if ($pendingCount -gt 0) {
Write-Output "`nUpdate Details:"
foreach ($update in $searchResult.Updates) {
Write-Output " - $($update.Title)"
}
}
} catch {
Write-Error "Failed to check updates: $_"
Ninja-Property-Set pendingUpdates -1
Ninja-Property-Set updateStatus "Error"
exit 1
}Script 3: BitLocker Status Check
<#
.SYNOPSIS
Checks BitLocker encryption status and recovery key availability.
.DESCRIPTION
Reports BitLocker status for all fixed drives and validates recovery key backup.
.NOTES
Schedule: Weekly
Run As: SYSTEM
#>
$results = @()
$allEncrypted = $true
try {
# Get all fixed drives
$volumes = Get-BitLockerVolume -ErrorAction Stop
foreach ($volume in $volumes) {
$status = @{
Drive = $volume.MountPoint
EncryptionStatus = $volume.VolumeStatus
ProtectionStatus = $volume.ProtectionStatus
EncryptionPercentage = $volume.EncryptionPercentage
KeyProtectors = ($volume.KeyProtector | ForEach-Object { $_.KeyProtectorType }) -join ", "
}
$results += $status
# Check if drive is fully encrypted
if ($volume.VolumeStatus -ne "FullyEncrypted") {
$allEncrypted = $false
}
}
# Update NinjaOne custom field
if ($allEncrypted -and $results.Count -gt 0) {
Ninja-Property-Set bitlockerStatus "Enabled"
} elseif ($results.Count -eq 0) {
Ninja-Property-Set bitlockerStatus "NotSupported"
} else {
Ninja-Property-Set bitlockerStatus "Partial"
}
# Output results
Write-Output "=== BitLocker Status ==="
foreach ($result in $results) {
Write-Output "Drive: $($result.Drive)"
Write-Output " Status: $($result.EncryptionStatus)"
Write-Output " Protection: $($result.ProtectionStatus)"
Write-Output " Encrypted: $($result.EncryptionPercentage)%"
Write-Output " Protectors: $($result.KeyProtectors)"
Write-Output ""
}
# Check for recovery key backup
$recoveryKey = $volumes | Where-Object { $_.KeyProtector.KeyProtectorType -contains "RecoveryPassword" }
if ($recoveryKey) {
Write-Output "Recovery Key: Present"
Ninja-Property-Set recoveryKeyBackedUp "Yes"
} else {
Write-Output "Recovery Key: Not Found"
Ninja-Property-Set recoveryKeyBackedUp "No"
}
} catch {
if ($_.Exception.Message -like "*not recognized*") {
Write-Output "BitLocker cmdlets not available (Windows Home?)"
Ninja-Property-Set bitlockerStatus "NotSupported"
} else {
Write-Error "Error checking BitLocker: $_"
Ninja-Property-Set bitlockerStatus "Error"
exit 1
}
}Step 4: Create Maintenance Automation
Build scripts for routine maintenance tasks.
Script 4: Disk Cleanup and Optimization
<#
.SYNOPSIS
Performs comprehensive disk cleanup and optimization.
.DESCRIPTION
Clears temp files, Windows Update cache, browser caches, and runs disk optimization.
.NOTES
Schedule: Weekly (maintenance window)
Run As: SYSTEM
#>
param(
[switch]$IncludeBrowserCache = $false,
[int]$OlderThanDays = 7
)
$totalFreed = 0
$cutoffDate = (Get-Date).AddDays(-$OlderThanDays)
Write-Output "=== Disk Cleanup Starting ==="
Write-Output "Clearing files older than: $cutoffDate"
# Function to safely remove files
function Remove-OldFiles {
param($Path, $Description)
if (Test-Path $Path) {
try {
$files = Get-ChildItem -Path $Path -Recurse -File -ErrorAction SilentlyContinue |
Where-Object { $_.LastWriteTime -lt $cutoffDate }
$size = ($files | Measure-Object -Property Length -Sum).Sum
$files | Remove-Item -Force -ErrorAction SilentlyContinue
$freedMB = [math]::Round($size / 1MB, 2)
Write-Output "$Description : Freed $freedMB MB"
return $size
} catch {
Write-Warning "Error cleaning $Description : $_"
return 0
}
}
return 0
}
# Clear Windows Temp folder
$totalFreed += Remove-OldFiles -Path "$env:SystemRoot\Temp" -Description "Windows Temp"
# Clear User Temp folders
$userProfiles = Get-ChildItem -Path "C:\Users" -Directory
foreach ($profile in $userProfiles) {
$tempPath = Join-Path $profile.FullName "AppData\Local\Temp"
$totalFreed += Remove-OldFiles -Path $tempPath -Description "User Temp ($($profile.Name))"
}
# Clear Windows Update Cache
$wuPath = "$env:SystemRoot\SoftwareDistribution\Download"
$totalFreed += Remove-OldFiles -Path $wuPath -Description "Windows Update Cache"
# Clear Windows Installer Cache (orphaned patches)
$installerPath = "$env:SystemRoot\Installer\$PatchCache$"
$totalFreed += Remove-OldFiles -Path $installerPath -Description "Installer Cache"
# Clear CBS Logs (older than 30 days)
$cbsPath = "$env:SystemRoot\Logs\CBS"
$totalFreed += Remove-OldFiles -Path $cbsPath -Description "CBS Logs"
# Clear Browser Caches (if requested)
if ($IncludeBrowserCache) {
foreach ($profile in $userProfiles) {
# Chrome
$chromePath = Join-Path $profile.FullName "AppData\Local\Google\Chrome\User Data\Default\Cache"
$totalFreed += Remove-OldFiles -Path $chromePath -Description "Chrome Cache ($($profile.Name))"
# Edge
$edgePath = Join-Path $profile.FullName "AppData\Local\Microsoft\Edge\User Data\Default\Cache"
$totalFreed += Remove-OldFiles -Path $edgePath -Description "Edge Cache ($($profile.Name))"
}
}
# Empty Recycle Bin
try {
Clear-RecycleBin -Force -ErrorAction SilentlyContinue
Write-Output "Recycle Bin: Emptied"
} catch {
Write-Warning "Could not empty Recycle Bin: $_"
}
# Run Disk Cleanup utility (silent)
Write-Output "Running Windows Disk Cleanup..."
$cleanupArgs = @(
"StateFlags0100" # Use profile 100
)
# Set cleanup profile
$regPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VolumeCaches"
$cleanupItems = @(
"Active Setup Temp Folders",
"Downloaded Program Files",
"Internet Cache Files",
"Recycle Bin",
"Temporary Files",
"Thumbnail Cache"
)
foreach ($item in $cleanupItems) {
$itemPath = Join-Path $regPath $item
if (Test-Path $itemPath) {
Set-ItemProperty -Path $itemPath -Name "StateFlags0100" -Value 2 -Type DWord -ErrorAction SilentlyContinue
}
}
Start-Process -FilePath "cleanmgr.exe" -ArgumentList "/sagerun:100" -Wait -NoNewWindow -ErrorAction SilentlyContinue
# Optimize drive (SSD TRIM or HDD defrag)
Write-Output "Optimizing drives..."
$volumes = Get-Volume | Where-Object { $_.DriveLetter -and $_.DriveType -eq 'Fixed' }
foreach ($vol in $volumes) {
try {
Optimize-Volume -DriveLetter $vol.DriveLetter -ErrorAction SilentlyContinue
Write-Output "Optimized: $($vol.DriveLetter):"
} catch {
Write-Warning "Could not optimize $($vol.DriveLetter): $_"
}
}
# Calculate final disk space
$disk = Get-CimInstance -ClassName Win32_LogicalDisk -Filter "DeviceID='C:'"
$freeSpaceGB = [math]::Round($disk.FreeSpace / 1GB, 2)
$totalFreedMB = [math]::Round($totalFreed / 1MB, 2)
# Update NinjaOne custom fields
Ninja-Property-Set diskSpaceGB $freeSpaceGB
Ninja-Property-Set lastCleanupDate (Get-Date -Format "yyyy-MM-dd")
Ninja-Property-Set lastCleanupFreedMB $totalFreedMB
Write-Output ""
Write-Output "=== Cleanup Complete ==="
Write-Output "Total Freed: $totalFreedMB MB"
Write-Output "Current Free Space: $freeSpaceGB GB"Script 5: Service Health Remediation
<#
.SYNOPSIS
Monitors critical services and automatically restarts failed services.
.DESCRIPTION
Checks specified services, attempts restart if stopped, and alerts on failure.
.NOTES
Schedule: Every 15 minutes (condition script)
Run As: SYSTEM
#>
param(
[string[]]$ServiceNames = @("Spooler", "wuauserv", "WinDefend", "EventLog")
)
$failedServices = @()
$restartedServices = @()
Write-Output "=== Service Health Check ==="
Write-Output "Checking services: $($ServiceNames -join ', ')"
Write-Output ""
foreach ($serviceName in $ServiceNames) {
try {
$service = Get-Service -Name $serviceName -ErrorAction Stop
if ($service.Status -ne 'Running') {
Write-Output "[$serviceName] Status: $($service.Status) - Attempting restart..."
# Check if service is set to Automatic
$serviceConfig = Get-CimInstance -ClassName Win32_Service -Filter "Name='$serviceName'"
if ($serviceConfig.StartMode -eq 'Auto' -or $serviceConfig.StartMode -eq 'Automatic') {
# Attempt to start the service
Start-Service -Name $serviceName -ErrorAction Stop
# Wait and verify
Start-Sleep -Seconds 5
$service = Get-Service -Name $serviceName
if ($service.Status -eq 'Running') {
Write-Output "[$serviceName] Successfully restarted"
$restartedServices += $serviceName
} else {
Write-Warning "[$serviceName] Failed to restart - Status: $($service.Status)"
$failedServices += $serviceName
}
} else {
Write-Output "[$serviceName] Not set to Auto-start (StartMode: $($serviceConfig.StartMode))"
}
} else {
Write-Output "[$serviceName] Running OK"
}
} catch {
Write-Error "[$serviceName] Error: $_"
$failedServices += $serviceName
}
}
# Update NinjaOne custom fields
if ($failedServices.Count -gt 0) {
Ninja-Property-Set serviceHealthStatus "Failed"
Ninja-Property-Set failedServices ($failedServices -join ", ")
Write-Output ""
Write-Output "ALERT: Failed services: $($failedServices -join ', ')"
exit 1
} else {
Ninja-Property-Set serviceHealthStatus "Healthy"
Ninja-Property-Set failedServices ""
if ($restartedServices.Count -gt 0) {
Write-Output ""
Write-Output "Restarted services: $($restartedServices -join ', ')"
}
exit 0
}Step 5: Implement Condition-Based Automation
Configure scripts that run based on device conditions.
Navigate to: Administration → Policies → Conditions
Create Disk Space Condition:
- Click + Add Condition
- Configure:
- Name:
Low Disk Space Alert - Condition: Custom Field → diskSpaceGB < 10
- Action: Run Script → Clear-TempFiles.ps1
- Severity: Warning
- Name:
Create Pending Reboot Condition:
- Condition: Custom Field → lastRebootDays > 30
- Action: Create Ticket / Send Notification
- Severity: Warning
Condition Script Example:
<#
.SYNOPSIS
Condition script to check if device requires attention.
.DESCRIPTION
Returns exit code 0 (condition met) or 1 (condition not met).
.NOTES
Type: Condition Script
Run As: SYSTEM
#>
# Check multiple conditions
$conditionMet = $false
$reasons = @()
# Check disk space
$disk = Get-CimInstance -ClassName Win32_LogicalDisk -Filter "DeviceID='C:'"
$freeSpaceGB = [math]::Round($disk.FreeSpace / 1GB, 2)
if ($freeSpaceGB -lt 10) {
$conditionMet = $true
$reasons += "Low disk space: $freeSpaceGB GB"
}
# Check uptime
$os = Get-CimInstance -ClassName Win32_OperatingSystem
$uptime = (Get-Date) - $os.LastBootUpTime
if ($uptime.TotalDays -gt 30) {
$conditionMet = $true
$reasons += "Uptime: $([math]::Round($uptime.TotalDays, 0)) days"
}
# Check pending updates
try {
$updateSession = New-Object -ComObject Microsoft.Update.Session
$searcher = $updateSession.CreateUpdateSearcher()
$result = $searcher.Search("IsInstalled=0 AND IsHidden=0")
$criticalUpdates = $result.Updates | Where-Object {
$_.Categories | Where-Object { $_.Name -eq "Critical Updates" }
}
if ($criticalUpdates.Count -gt 0) {
$conditionMet = $true
$reasons += "Critical updates pending: $($criticalUpdates.Count)"
}
} catch {
Write-Warning "Could not check updates: $_"
}
# Return result
if ($conditionMet) {
Write-Output "Condition Met: $($reasons -join '; ')"
exit 0 # Condition MET
} else {
Write-Output "Device healthy - no conditions met"
exit 1 # Condition NOT met
}Step 6: Create Scheduled Automation
Set up time-based script execution for maintenance.
Navigate to: Administration → Library → Scripting → (Select Script) → Scheduled Tasks
Common Schedules:
| Script | Schedule | Run As |
|---|---|---|
| System Info Collection | Daily 6:00 AM | SYSTEM |
| Disk Cleanup | Weekly Sunday 2:00 AM | SYSTEM |
| Update Check | Daily 7:00 AM | SYSTEM |
| Certificate Expiry Check | Weekly | SYSTEM |
| User Activity Report | Daily (end of day) | SYSTEM |
Create Scheduled Task:
- Select script from library
- Click Schedule
- Configure:
- Frequency: Daily/Weekly/Monthly
- Time: Specify execution time
- Timezone: Organization timezone
- Target: All devices / specific policies
Script: Certificate Expiry Monitoring
<#
.SYNOPSIS
Checks for expiring certificates and reports to NinjaOne.
.DESCRIPTION
Scans local machine certificate store for certificates expiring within threshold.
.NOTES
Schedule: Weekly
Run As: SYSTEM
#>
param(
[int]$ExpiryThresholdDays = 30
)
$expiringCerts = @()
$expiredCerts = @()
$thresholdDate = (Get-Date).AddDays($ExpiryThresholdDays)
Write-Output "=== Certificate Expiry Check ==="
Write-Output "Checking certificates expiring before: $($thresholdDate.ToString('yyyy-MM-dd'))"
Write-Output ""
# Check Local Machine store
$stores = @(
"Cert:\LocalMachine\My",
"Cert:\LocalMachine\WebHosting",
"Cert:\LocalMachine\Root"
)
foreach ($store in $stores) {
if (Test-Path $store) {
$certs = Get-ChildItem -Path $store | Where-Object {
$_.NotAfter -lt $thresholdDate -and $_.NotAfter -gt (Get-Date).AddYears(-1)
}
foreach ($cert in $certs) {
$certInfo = @{
Subject = $cert.Subject
Thumbprint = $cert.Thumbprint
Expiry = $cert.NotAfter
Store = $store
DaysRemaining = [math]::Round(($cert.NotAfter - (Get-Date)).TotalDays, 0)
}
if ($cert.NotAfter -lt (Get-Date)) {
$expiredCerts += $certInfo
Write-Output "EXPIRED: $($cert.Subject)"
Write-Output " Expired: $($cert.NotAfter)"
} else {
$expiringCerts += $certInfo
Write-Output "EXPIRING: $($cert.Subject)"
Write-Output " Expires: $($cert.NotAfter) ($($certInfo.DaysRemaining) days)"
}
}
}
}
# Update NinjaOne custom fields
$totalIssues = $expiringCerts.Count + $expiredCerts.Count
if ($expiredCerts.Count -gt 0) {
Ninja-Property-Set certificateStatus "Expired"
} elseif ($expiringCerts.Count -gt 0) {
Ninja-Property-Set certificateStatus "Expiring"
} else {
Ninja-Property-Set certificateStatus "Healthy"
}
Ninja-Property-Set expiringCertCount $expiringCerts.Count
Ninja-Property-Set expiredCertCount $expiredCerts.Count
Write-Output ""
Write-Output "=== Summary ==="
Write-Output "Expired: $($expiredCerts.Count)"
Write-Output "Expiring within $ExpiryThresholdDays days: $($expiringCerts.Count)"
if ($totalIssues -gt 0) {
exit 1 # Alert condition
} else {
exit 0
}Step 7: Build Error Handling Framework
Implement consistent error handling across scripts.
Error Handling Template:
<#
.SYNOPSIS
Template with comprehensive error handling for NinjaOne scripts.
.DESCRIPTION
Includes try/catch, logging, and proper exit codes.
#>
# Script configuration
$ErrorActionPreference = "Stop"
$scriptName = "Script-Name"
$logPath = "$env:ProgramData\NinjaRMM\Logs"
# Ensure log directory exists
if (-not (Test-Path $logPath)) {
New-Item -ItemType Directory -Path $logPath -Force | Out-Null
}
$logFile = Join-Path $logPath "$scriptName-$(Get-Date -Format 'yyyyMMdd').log"
# Logging function
function Write-Log {
param(
[string]$Message,
[ValidateSet("INFO", "WARNING", "ERROR")]
[string]$Level = "INFO"
)
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$logEntry = "[$timestamp] [$Level] $Message"
# Write to console
switch ($Level) {
"INFO" { Write-Output $logEntry }
"WARNING" { Write-Warning $Message }
"ERROR" { Write-Error $Message }
}
# Write to log file
Add-Content -Path $logFile -Value $logEntry -ErrorAction SilentlyContinue
}
# Main script execution
try {
Write-Log "Script started"
# === YOUR SCRIPT LOGIC HERE ===
# Example operation with error handling
try {
$result = Get-Something -Parameter $value
Write-Log "Operation completed: $result"
} catch {
Write-Log "Operation failed: $_" -Level "WARNING"
# Continue or throw based on severity
}
# === END SCRIPT LOGIC ===
Write-Log "Script completed successfully"
Ninja-Property-Set lastScriptStatus "Success"
Ninja-Property-Set lastScriptRun (Get-Date -Format "yyyy-MM-dd HH:mm:ss")
exit 0
} catch {
Write-Log "Script failed: $_" -Level "ERROR"
Write-Log "Stack trace: $($_.ScriptStackTrace)" -Level "ERROR"
Ninja-Property-Set lastScriptStatus "Failed"
Ninja-Property-Set lastScriptError $_.Exception.Message
Ninja-Property-Set lastScriptRun (Get-Date -Format "yyyy-MM-dd HH:mm:ss")
exit 1
} finally {
# Cleanup code (always runs)
Write-Log "Script execution ended"
}Step 8: Test and Deploy Scripts
Validate scripts before production deployment.
Testing Workflow:
| Phase | Scope | Duration |
|---|---|---|
| 1. Development | Single test device | Until working |
| 2. Pilot | 5-10 devices across types | 1 week |
| 3. Limited rollout | One client/location | 1 week |
| 4. Full deployment | All devices | Ongoing |
Test Script Locally:
# Mock NinjaOne cmdlets for local testing
function Ninja-Property-Set {
param($Name, $Value)
Write-Host "[NINJA] Set $Name = $Value" -ForegroundColor Cyan
}
# Run script
.\Your-Script.ps1Monitor Script Execution:
- Navigate to Activities → Script Results
- Filter by script name
- Review:
- Execution time
- Exit codes
- Output/errors
Common Exit Codes:
| Code | Meaning | Action |
|---|---|---|
| 0 | Success | None |
| 1 | General failure | Check output |
| 2 | Condition not met | Expected for conditions |
| 1603 | Installation failed | Check installer logs |
Troubleshooting
Common Issues:
| Symptom | Possible Cause | Solution |
|---|---|---|
| Script timeout | Long-running operation | Increase timeout; optimize script |
| Custom field not updating | Wrong field name | Verify exact field name (case-sensitive) |
| Access denied | Wrong Run As context | Use SYSTEM for system operations |
| Module not found | Module not installed | Include module installation in script |
| Encoding issues | Non-ASCII characters | Save script as UTF-8 with BOM |
Debug Script Execution:
# Add verbose logging
$VerbosePreference = "Continue"
Write-Verbose "Debug message here"
# Output variable contents
Write-Output "Variable value: $($variable | ConvertTo-Json)"
# Test connectivity
Test-NetConnection -ComputerName "api.ninjarmm.com" -Port 443Check Script Logs:
- Navigate to device in NinjaOne
- Click Activities tab
- Filter by Scripts
- Click script run to view output
Security Considerations
Script Security Best Practices:
| Practice | Implementation |
|---|---|
| Least privilege | Use specific permissions, not full admin |
| Input validation | Validate all parameters |
| Secure credentials | Use NinjaOne secrets, not plain text |
| Logging | Log actions but not sensitive data |
| Code signing | Sign scripts with certificate |
Handling Credentials:
# Use NinjaOne Script Variables for secrets
# Configure in script settings, reference as:
$apiKey = $env:NINJA_SECRET_APIKEY
# Or use NinjaOne Credential fields
$credential = Ninja-Property-Get "serviceAccountCred"Avoid:
# NEVER hardcode credentials
$password = "PlainTextPassword123" # BAD
# NEVER log sensitive data
Write-Output "Password is: $password" # BADVerification Checklist
Script Development:
- Script tested on local machine
- Error handling implemented
- Logging added for troubleshooting
- Custom fields correctly referenced
- Exit codes properly set
Deployment:
- Script added to library with description
- Categories/tags assigned
- Appropriate Run As context selected
- Tested on pilot group
- Schedule configured (if applicable)
Operations:
- Monitoring dashboard created
- Alert conditions configured
- Documentation updated
- Team trained on script usage
Next Steps
After building your script library:
- Create Reporting Dashboards - Visualize custom field data
- Build Automation Workflows - Chain scripts together
- Integrate with Ticketing - Auto-create tickets from scripts
- Develop Self-Healing Automation - Condition + remediation pairs
References
- NinjaOne Scripting Documentation
- PowerShell Best Practices
- NinjaOne Custom Fields Guide
- NinjaOne API Documentation
Last Updated: February 2026