07 Jan Installomator – Post-Install Policies, Triggers, and Follow-Up Actions
Overview
Installomator is an open-source shell script that automates the installation and updating of macOS applications in enterprise and managed environments. It uses pre-configured “labels,” which act as recipes describing how to download, verify, and install specific applications. Each label typically contains the download URL, application name, file type (DMG, ZIP, PKG), and the Apple Developer Team ID used to verify the authenticity of the downloaded software.
During an installation workflow, Installomator downloads the application, mounts disk images or extracts archives as needed, copies the application into the appropriate location, and cleans up temporary files. It also produces detailed logs, which are useful for troubleshooting and auditing.
Installomator is particularly valuable in environments using MDM solutions like Jamf Pro, Mosyle, or Kandji because it standardizes software deployment across the fleet. Instead of writing bespoke install scripts for each application, administrators can rely on Installomator’s library of labels that cover hundreds of popular macOS apps, or they can define new labels following the same format.
Additional built-in capabilities include version checking (to avoid unnecessary downloads when the latest version is already installed), Team ID verification (to ensure binaries are legitimate), and support for multiple installation types (direct app bundle installation, PKG installers, and in-place application updates). Combined, these features help maintain consistent, secure, and up-to-date software across managed Mac environments.
Using Jamf Post-Install Workflows with Installomator
When combined with Jamf Pro or another MDM, Installomator usually serves as the primary installer in a larger workflow. The MDM policy triggers Installomator to install or update the target app, and then follow-up actions are used to finish configuration or cleanup.
- Common post-install tasks include:
- Installing or applying license files.
- Moving
.appbundles to a custom directory (for example,/Applications/Audio). - Cleaning up older versions or conflicting installations.
- Installing related components such as command-line tools, plug-ins, or additional dependencies.
- Other post-install actions like Jamf Recon, etc.
The following patterns describe how to structure those post-install tasks in Jamf Pro.
Pattern A – Single Jamf Policy with Priority Ordering
In this pattern, a single Jamf policy is responsible for both the main Installomator run and all related post-install steps. Package and script priorities within the policy are used to control ordering.
The Installomator payload is configured to run first, using a “Before” priority or equivalent high-priority order, to install or update the main application. Post-install scripts and packages are configured to run after Installomator using “After” or lower priorities.
- This pattern is useful when you want a self-contained workflow for a single application where all required tasks happen in a guaranteed order. Example uses include:
- Installing license files or configuration data immediately after the main application is present.
- Moving the installed
.appbundle into a custom location, such as/Applications/Audioanother custom sub-folder. - Removing old versions, legacy bundles, or conflicting installations that might coexist with the new app.
Pattern A – Example Workflow
Amadeus Pro
Amadeus Pro is a multitrack audio editor for macOS that acts as a kind of “Swiss army knife” for sound editing, letting you record, edit, and process audio in formats like MP3, AAC, Ogg Vorbis, Apple Lossless, AIFF, and WAV, among others. It supports multitrack projects where each track’s volume and panning can be adjusted independently, tracks can be split into movable clips, and Audio Unit plug-ins can be applied non-destructively in real time. The app includes built-in effects (such as EQ, speed and pitch changes, echo) and a powerful batch processor for converting large sets of files while applying effect chains automatically, making it useful for tasks like digitizing tapes, cleaning up recordings, or preparing podcasts. Amadeus Pro also offers tools for repairing audio (like click removal and denoising) and analyzing sound, and thanks to direct-to-disk editing and waveform caching, it can handle very large audio files efficiently while remaining responsive even on lengthy projects.
Because Amadeus Pro is a commercial application, a license must be installed after Installomator completes the initial installation for the software to function properly.
Here is an example of the installation policy that is used with Amadeus Pro for user Self Service installation, or a similar one for updates with Installomator that is executed on a schedule.
Scripts – Installomator Script and Script Parameters
Here is an example of the Installomator script and script parameters, where the priority is set to “Before”.
Packages – Amadeus Pro License Installer Package
Next, there is an installer package that installs our Amadeus Pro license.
For reference, Amadeus Pro installs the license in the location “/Library/Preferences/com.HairerSoft.AmadeusPro.plist” and you can use a tool like munkipkg, The Luggage, pkggen, PackageMaker, Packages, Composer, or QuickPkg to create an installer package for the license file distribution.
We use munkipkg because it is open source and supported by an active community of MacAdmins and developers, it is collaboration-friendly since projects can be easily checked into Git and shared across teams, it produces standard Apple installer packages, it fits well into CI/CD workflows and other automated build processes, and it uses human-readable configuration formats such as YAML, JSON, or plist, making it straightforward to review and maintain.
Pattern B – Custom Triggers for Related Tools and Components
In this pattern, Installomator still performs the primary installation, but related tasks are broken out into separate Jamf policies that are invoked via custom triggers. This approach favors modularity and reuse of component policies.
Individual component policies are created for related tools and dependencies. Each of these policies is assigned a descriptive custom trigger, such as rstudio, xcode_command_line_tools, or r_for_mac. The policies themselves handle installing or updating those components, along with any of their own post-install steps.
A post-install script in the primary Installomator policy calls these component policies using the Jamf CLI. For example, it might run jamf policy -event xcode_command_line_tools and jamf policy -event r_for_mac once the main application has been successfully installed.
This pattern is valuable for complex stacks where multiple tools must be installed together (for example, RStudio, R for Mac, and Xcode Command Line Tools) but where each component also needs to be maintainable and deployable independently.
Pattern B – Example Workflow
RStudio
RStudio for Mac is just the IDE—it doesn’t include the R language or a compiler toolchain—so installing R for Mac and Xcode Command Line Tools gives users a complete, working environment. R for Mac provides the actual R interpreter, standard libraries, and documentation that RStudio connects to; without it, RStudio can’t run code and will complain that R isn’t installed. Xcode Command Line Tools adds the compilers and build utilities (like clang, make, and headers/SDKs) that many R packages need to compile C/C++/Fortran code from source on macOS, which is common for performance-sensitive or specialized CRAN and Bioconductor packages. Together, RStudio (IDE) + R for Mac (runtime) + Xcode CLT (toolchain) greatly reduce errors such as failed install.packages() calls or RStudio being unable to find R, and from a Mac admin standpoint, deploying all three ensures users get a ready-to-use R environment with minimal support issues.
Scripts – Installomator Script and Script Parameters
Here is an example of the Installomator script and script parameters, where the priority is set to “Before”.
Files and Processes – Execute Command
Next, you want to install “Xcode Command Line Tools” and “R for Mac”. In this example, we will outline the steps using Jamf Pro policies “File and Processes” option.
To set this up in Jamf Pro, first create two separate policies: one that installs R for Mac and assigns it the Custom Trigger r_for_mac, and another that installs Xcode Command Line Tools with the Custom Trigger xcode_command_line_tools. Then, in the policy that should kick off both installs (for example, your “RStudio” policy), go to Files and Processes → Execute Command and enter a command that calls both triggers.
If you only want the Xcode Command Line Tools policy to run when the R for Mac policy succeeds, use: /usr/local/jamf/bin/jamf policy -event r_for_mac && /usr/local/jamf/bin/jamf policy -event xcode_command_line_tools.
If you want both policies to run regardless of whether the first one succeeds, use: /usr/local/jamf/bin/jamf policy -event r_for_mac ; /usr/local/jamf/bin/jamf policy -event xcode_command_line_tools.
Pattern C – Parameter-Driven Jamf Policy Runner Script
A parameter-driven policy runner script provides a more generic way to run multiple Jamf policies from a single script. It allows you to pass per-policy parameters, suppress recon during individual installs, and capture clear logging about success or failure for each invocation. This pattern is designed for Jamf Pro and uses script parameters 4 through 11.
Bash Script – Jamf Pro Multi-Policy Executor with Parameter Support
The following script is a “multi-policy runner” for Jamf Pro that lets you trigger several Jamf policies from a single script by using script parameters 4–11 as custom event names. When the policy runs, it validates that the Jamf binary exists, loops through each non-empty parameter, sanity-checks the trigger name, and then runs jamf policy -event <trigger> -verbose(optionally wrapped in a 5-minute timeout), logging success or failure for each run to both the console and a log file. It skips empty parameters, waits a few seconds between executions so it doesn’t hammer the Jamf server, tracks how many policies ran/failed/skipped, and exits with a non-zero status if any policy fails, giving you a clear, centralized way to orchestrate multiple custom-trigger policies in one place.
#!/usr/bin/env bash
#
# Jamf Pro Multi-Policy Executor with Parameter Support
#
# This script executes multiple Jamf policies using provided parameters and logs results.
#
# Revised - 2026.01.07
#
# Usage:
# - Parameters 4-11 are passed as policy triggers to separate jamf policy executions
# - Each non-empty parameter triggers a separate policy run with that trigger
# - Logs success/failure status for each policy execution
# - Example: Set Parameter 4 to "install_chrome" to trigger policies with that event
#
# Jamf Pro Configuration:
# - Add this script to a Jamf Pro policy
# - Configure Parameters 4-11 with your desired policy triggers
# - Script will execute "jamf policy -event <parameter>" for each non-empty parameter
#
# Copyright (c) 2025 University of Utah, Marriott Library ITS
# All Rights Reserved.
#
# Permission to use, copy, modify, and distribute this software and
# its documentation for any purpose and without fee is hereby granted,
# provided that the above copyright notice appears in all copies and
# that both that copyright notice and this permission notice appear
# in supporting documentation, and that the name of The University
# of Utah not be used in advertising or publicity pertaining to
# distribution of the software without specific, written prior
# permission. This software is supplied as is without expressed or
# implied warranties of any kind.
#
set -euo pipefail # Exit on error, undefined vars, pipe failures
##################################
# Global Variables
##################################
readonly JAMF_BINARY="/usr/local/jamf/bin/jamf"
readonly SCRIPT_NAME
SCRIPT_NAME="$(basename "$0")"
readonly LOG_FILE="/var/log/jamf_multi_policy.log"
# Jamf script parameters (4-11) - these are set by Jamf Pro when the script runs
readonly SCRIPT_PARAMETERS=(
"${4:-}" # Parameter 4: First policy trigger
"${5:-}" # Parameter 5: Second policy trigger
"${6:-}" # Parameter 6: Third policy trigger
"${7:-}" # Parameter 7: Fourth policy trigger
"${8:-}" # Parameter 8: Fifth policy trigger
"${9:-}" # Parameter 9: Sixth policy trigger
"${10:-}" # Parameter 10: Seventh policy trigger
"${11:-}" # Parameter 11: Eighth policy trigger
)
##################################
# Functions
##################################
# Enhanced logging function - logs to both console and file
log()
{
local level="$1"
shift
local timestamp="$(date '+%Y-%m-%d %H:%M:%S')"
local message="${timestamp} - ${SCRIPT_NAME} - ${level}: $*"
echo "$message"
# Only write to log file if it's writable (avoid errors)
if [[ -w "$LOG_FILE" ]] || [[ -w "$(dirname "$LOG_FILE")" ]]; then
echo "$message" >> "$LOG_FILE" 2>/dev/null || true
fi
}
# Validate jamf binary exists and is executable
validate_jamf_binary()
{
if [[ ! -x "$JAMF_BINARY" ]]; then
log "ERROR" "Jamf binary not found or not executable: $JAMF_BINARY"
exit 1
fi
log "INFO" "Jamf binary validated: $JAMF_BINARY"
}
# Execute a single jamf policy with trigger
execute_policy()
{
local trigger="$1"
local param_number="$2"
log "INFO" "Executing jamf policy -event \"${trigger}\" (Parameter ${param_number})"
# Add timeout to prevent hanging policies
local timeout_cmd=""
if command -v timeout >/dev/null 2>&1; then
timeout_cmd="timeout 300" # 5 minute timeout
fi
# Use -verbose for better logging in Jamf Pro logs
if $timeout_cmd "$JAMF_BINARY" policy -event "$trigger" -verbose; then
log "SUCCESS" "Policy trigger \"${trigger}\" executed successfully"
return 0
else
local exit_code=$?
if [[ $exit_code -eq 124 ]]; then
log "ERROR" "Policy trigger \"${trigger}\" timed out after 5 minutes"
else
log "ERROR" "Policy trigger \"${trigger}\" failed (exit code: ${exit_code})"
fi
return $exit_code
fi
}
# Validate trigger name (enhanced safety check)
validate_trigger()
{
local trigger="$1"
# Check for empty trigger
if [[ -z "$trigger" ]]; then
log "ERROR" "Empty trigger provided"
return 1
fi
# Check for potentially dangerous characters (more restrictive)
if [[ "$trigger" =~ [^a-zA-Z0-9_.-] ]]; then
log "WARN" "Trigger contains potentially unsafe characters: $trigger"
return 1
fi
# Check for dangerous patterns
if [[ "$trigger" =~ ^-|\.\.|\|| ]]; then
log "WARN" "Trigger contains dangerous patterns: $trigger"
return 1
fi
# Check length (reasonable limit)
if [[ ${#trigger} -gt 100 ]]; then
log "WARN" "Trigger name too long (>100 chars): $trigger"
return 1
fi
# Check minimum length
if [[ ${#trigger} -lt 2 ]]; then
log "WARN" "Trigger name too short (<2 chars): $trigger"
return 1
fi
return 0
}
##################################
# Main Execution
##################################
main()
{
log "INFO" "Starting Jamf Pro multi-policy execution"
log "INFO" "Script version: 2025.09.05"
log "INFO" "Running as user: $(whoami)"
log "INFO" "Process ID: $$"
# Ensure log file is writable with better error handling
if ! touch "$LOG_FILE" 2>/dev/null; then
echo "WARNING: Cannot write to log file: $LOG_FILE"
if [[ -w "/tmp" ]]; then
LOG_FILE="/tmp/jamf_multi_policy_$$.log"
echo "Using fallback log file: $LOG_FILE"
else
echo "WARNING: Cannot create fallback log file, logging to console only"
LOG_FILE="/dev/null"
fi
fi
validate_jamf_binary
local failed_policies=0
local executed_policies=0
local skipped_policies=0
local param_count=3 # Start at 3 since we're using parameters 4-11
# Log all parameters for debugging
log "DEBUG" "Script parameters: ${SCRIPT_PARAMETERS[*]}"
for trigger in "${SCRIPT_PARAMETERS[@]}"; do
((param_count++))
# Skip empty parameters
if [[ -z "$trigger" ]]; then
log "DEBUG" "Skipping empty Parameter ${param_count}"
((skipped_policies++))
continue
fi
# Validate trigger before execution
if ! validate_trigger "$trigger"; then
log "ERROR" "Invalid trigger name in Parameter ${param_count}: $trigger"
((failed_policies++))
continue
fi
((executed_policies++))
if ! execute_policy "$trigger" "$param_count"; then
((failed_policies++))
fi
# Add delay between policy executions to prevent overwhelming Jamf server
# Skip delay after last policy
if [[ $param_count -lt 11 ]]; then
log "DEBUG" "Waiting 3 seconds before next policy execution..."
sleep 3
fi
done
# Summary
log "INFO" "Execution Summary:"
log "INFO" " - Policies executed: ${executed_policies}"
log "INFO" " - Policies failed: ${failed_policies}"
log "INFO" " - Parameters skipped: ${skipped_policies}"
log "INFO" " - Total execution time: $(($(date +%s) - START_TIME)) seconds"
if [[ $failed_policies -gt 0 ]]; then
log "ERROR" "${failed_policies} policy trigger(s) failed"
exit 1
else
log "SUCCESS" "All policy triggers executed successfully"
exit 0
fi
}
# Add START_TIME at the beginning
readonly START_TIME
START_TIME=$(date +%s)
# Execute main function with all arguments
main "$@"
Scripts – Trigger File – Run Jamf Policy via Trigger File
Add the script to your Jamf Pro server, and then add it to the installation policy. Include the names of the trigger files and the corresponding script parameters.
Jamf Pro Policy Configuration (Script tab):
- Parameter 4:
rstudio - Parameter 5:
r_for_mac - Parameter 6:
xcode_command_line_tools - Parameters 7–11: (leave blank or unused)
What happens when this policy runs:
- The script validates the Jamf binary and each non-empty parameter.
- It then runs, in order:
jamf policy -event rstudio -verbosejamf policy -event r_for_mac -verbosejamf policy -event xcode_command_line_tools -verbose
- Each policy’s success or failure is logged to
/var/log/jamf_multi_policy.log(and the console). - Empty parameters are logged as “skipped,” and the script exits with a non-zero status only if one or more triggers fail.
Conditional Recon Strategy with Installomator and Jamf
A key operational goal is to avoid running Jamf recon when no software has actually changed, while still ensuring inventory is updated when apps are installed or updated. The patterns above can be combined to achieve more predictable and efficient recon behavior.
In the parameter-driven runner pattern, each Jamf policy invocation is made with -forceNoRecon, which guarantees that child policies do not trigger inventory updates. Recon is then controlled from a higher-level decision point:
- The runner script or its parent policy evaluates whether any policy failed or whether any installation occurred.
- Based on exit status, logs, or flags, it can then decide to run a single
jamf recononce, at the end of the workflow, rather than multiple times.
If you are using Installomator with a UI front-end such as SwiftDialog, you can also implement conditional recon by inspecting Installomator’s output. For example, some workflows check for messages such as “Latest version already installed” and skip recon when no update occurred, while allowing recon to proceed only when a new version is deployed.
Combining these techniques provides fine-grained control: Installomator and child policies focus on installations, while a single parent workflow decides when inventory should be updated based on actual changes.
Conditional Recon Strategy – Example Conditional Recon Flow
When using Installomator with a SwiftDialog front end, you can make Jamf recon truly conditional by inspecting the SwiftDialog command file at the end of the workflow and only running jamf recon if an actual install or update occurred.
The zz_Quit_SwiftDialog.sh helper script in the Installomator repository does this by parsing dialog_command_file for the text that Installomator writes when no update is needed. If that string is found, the script logs that nothing was installed and explicitly disables recon; otherwise, recon can proceed.
Core logic (from zz_Quit_SwiftDialog.sh):
# Go through dialog_command_file to figure out if it did not install anything
if grep "Latest version already installed" "$dialog_command_file"; then
echo "$(date +%F\ %T) : No Installomator installation happened. No Jamf recon"
jamf_recon=0
fi
Typical flow with this pattern:
- Installomator + SwiftDialog run and write status messages to
$dialog_command_file. - At the end of the run,
zz_Quit_SwiftDialog.shreads the file. - If it finds
Latest version already installed, it setsjamf_recon=0and logs that no recon will be run. - If that string is not found,
jamf_reconremains enabled (for example,1or non-empty) and a later block in the same script (or a follow-up script) will conditionally calljamf recononly whenjamf_reconis set.
Conditional Recon Pseudo-Flow
If jamf_recon=0
- → Skip
jamf reconentirely. - → Log: “No Installomator installation happened. No Jamf recon.”
If jamf_recon!=0 (or jamf_recon=1)
- → Run a single
jamf recononce at the end of the workflow. - → Recommended location for
jamf reconcall:- Same script, in a final block such as:
if [[ "$jamf_recon" != "0" ]]; then jamf recon; fi
- Or in a separate follow-up script/policy that only runs when the main policy reports an install/update (e.g., via exit code or a marker file).
- Same script, in a final block such as:








Graham Pugh
Posted at 05:01h, 08 JanuaryI’d just like to add the option of using AutoPkg to create the license package. This allows the entire building block of license file, scripts, groups and policy to be created in a single, version-controlled workflow suitable for CI-CD, which could also include creating the policy that runs Installomator.
u0105821
Posted at 08:49h, 08 JanuaryHi Graham,
Thanks for the feedback—that’s an excellent point about AutoPkg’s strengths in CI/CD workflows!
You’re absolutely right that AutoPkg shines when you need version-controlled, reproducible builds of complete deployment packages, including license files, scripts, groups, and policies. For organizations with established CI/CD pipelines or those requiring strict auditability and infrastructure-as-code approaches, AutoPkg’s recipe-based system is purpose-built for exactly this use case. AutoPkg excels at version pinning, staged testing, and creating entire deployment workflows in a single, automated pipeline—making it ideal for enterprise environments where updates must be validated before distribution and where every component needs to be tracked in version control. The trade-off is that AutoPkg requires more technical expertise, infrastructure investment, and ongoing maintenance (dedicated Mac system, recipe management, trust verification, storage infrastructure, and integration setup).
Installomator takes a different approach: it’s designed for speed and simplicity, handling the app installation piece with minimal configuration while leaving license management, policy creation, and other deployment components to your existing MDM workflows.
The hybrid approach:
You don’t have to choose one exclusively. Many organizations use both strategically—deploying most applications through Installomator for efficiency, while reserving other methods (manual, Jamf Pro Patch Management & Title Editor, Jamf Pro App Installers, AutoPkg, etc.) for critical software that requires version control, thorough testing, or custom packaging before release.
For organizations not using AutoPkg, Installomator integrates with several open-source projects that extend its functionality:
app-auto-update – https://github.com/App-Auto-Patch/App-Auto-Patch
App Auto-Patch is a macOS patch management script that discovers locally installed apps, uses Installomator for updates, and presents user-friendly swiftDialog prompts to guide users through patching. It’s designed to simplify app lifecycle management in environments like Jamf Pro by automating inventory, update workflows, deferrals, and reporting across managed Macs.
University of Utah MacAdmins Presentation: https://stream.lib.utah.edu/index.php?c=details&id=13711
Patchomator – https://github.com/Mac-Nerd/patchomator
Patchomator is a management script that extends Installomator into a general-purpose macOS patching tool, automatically discovering installed apps that have Installomator labels and updating any that are out of date. It supports discovery, configuration files, ignored/required label lists, and interactive or non-interactive workflows, making it well-suited for use in MDM environments.
Intuneomator – https://github.com/gilburns/Intuneomator
Intuneomator is a Swift-based macOS enterprise app that connects Installomator with Microsoft Intune to fully automate macOS app lifecycle management—discovering, packaging, uploading, and deploying apps at scale. It adds scheduled automation, Teams notifications, CVE-aware alerts, and secure Entra ID–based authentication to streamline Intune software management for Mac fleets.
University of Utah MacAdmins Presentation:
https://stream.lib.utah.edu/index.php?c=details&id=13722
Both approaches have merit—it really depends on your organization’s technical resources, infrastructure maturity, and deployment requirements.