Automating Jamf Pro App Installers
Our organization was looking for a way to patch third party applications on our Apple devices. As a Jamf Pro customer, a few options jumped out as potentially viable, but none could really compete with the attractive autonomy of App Installers. Out of the box, they're an excellent solution - Let's take a look at how you can make them even better.
This article has been converted from a session presented at JNUC 2023.
The complete codebase on which this article is based and additional documentation pertinent to the necessary API endpoints can be found on Github here.
App Installers
Primer
App Installers were introduced back in 2022 in Jamf Pro 10.37 as a Preview feature for Jamf Pro Cloud customers. They were and are essentially a curated catalog of Jamf-managed and Jamf-provided installer packages for a variety of applications.
The overall process is as follows:
- Create a Smart Group which will act as the scope for your deployment
- Add your App Installer from the Jamf Pro Catalog
- Apply the scope you created in step 1
- Configure as one sees fit:
- Install Automatically or deploy via Self Service
- Determine your end user experience
- For example, hard deadlines, notifications and their frequency, etc.
- Save, which will trigger your deployment
Gaps
No technology is perfect - Let's talk about a few gaps we hope to address with the following automations.
- As of today, there are 140+ App Installers available - How is one informed of catalog updates?
- How do you know if an App Installer is worth deploying?
- IE, is a deployment worth any potential risk or additional testing?
- Once deployed, failures are practically unavoidable due to the appreciable number of moving parts involved - So deployments will require the occasional 'retry' to get them to stick
Primary Automations

Detection and Suggestion
A rough overview of these steps goes something like this:
- Retrieve your environment's application inventory (how much of what is installed where)
- Retrieve the Jamf Pro Catalog's listing
- Get an intersection of the previous two arrays
- Remove any App Installers that aren't of interest:
- Already deployed
- Ignored
- Etc
- Send a Slack DM to a specified channel to notify your desired audience that an App Installer of interest is available for deployment
A more complete proof of concept implementation can be found on Github here.
Installation Prevalence
Arguably the most important basis for suggestion of App Installers is what an organization has installed and where.
Retrieve Your Inventory
One option is to grab this information from the Jamf Pro API when you run your automation, like so:
import requests
def get_computer_inventory(self, sections: List = None, page_size: int = 100,
sort: List = None, filter: str = None) -> List:
"""
Returns List of computer inventory records.
"""
page = 0
params = {'page-size': page_size}
params['sort'] = ','.join(sort) if sort else None
params['section'] = sections if sections else None
response = self.session.get(f'{self.base_url}/v1/computers-inventory',
params=params)
if response.status_code == 200:
results = response.json()['results']
print(len(results))
total = response.json()['totalCount']
total_pages = (total//page_size)
while page != total_pages:
page += 1
params['page'] = page
response = self.session.get(f'{self.base_url}/v1/computers-inventory',
params=params)
if response.status_code == 200:
results += response.json()['results']
print(len(results))
else:
print('Failed iteration')
return [{'Error': f'Failed iteration - {response.status_code}'}]
return results
else:
print('Failed to retrieve records')
return [{'Error': f'Failed to retrieve records - {response.status_code}'}]
Then parse the resultant inventory - You don't have to use this format, but it works for us:
def parse_jamf_apps(installed_apps: List) -> Dict:
bundle_dict = {}
for record in installed_apps:
for app in record['applications']:
if app['bundleId'] in bundle_dict.keys():
bundle_dict[app['bundleId']]['count'] += 1
else:
bundle_dict[app['bundleId']] = {"count": 1, "app_name": app['name']}
installed_app_lst = [{"app.bundleId": key,
"app.name": bundle_dict[key]['app_name'],
"count": bundle_dict[key]['count']} for key in bundle_dict.keys()]
return installed_app_lst
Receive Your Inventory
If your SIEM supports it, you could have it periodically send over your application inventory instead. Splunk, for example, supports either sending this information over via email - Or, preferably, via Webhook. Although if you choose the latter you may have to do some creative formatting as Splunk only likes to send over the first row of results:

What App Installers are available?
Let's get this information by checking the Jamf Pro Catalog's API. It's lazy to set a maximum 'page-size' to avoid paginating, but that seems like a problem for a later day.
def get_all_app_installers(self):
"""
Retrieve a full List of App Installers from the Jamf Pro Catalog.
"""
response = self.session.get(f'{self.base_url}/v1/app-installers/titles',
json={'page-size': 999})
if response.status_code == 200:
return response.json()
else:
print("Error retrieving App Installers", response.status_code, response.content)
Optional - Exclude "Ignored" App Installers
Occasionally, an App Installer will come up that may be too risky to deploy or not "worth" deploying for one reason or another. It can be helpful to maintain a list of App Installers you'd like to ignore and exclude them from future suggestions.
Configuration and Deployment
This stage focuses on what happens after a deployment is approved and generally aligns to the following steps:
- Receive approval for deployment
- Check for an existing Smart Group (and create one if necessary)
- Add our App Installer from the Catalog and configure it accordingly
- (Optionally) Add a corresponding Patch Software Title to track metrics
Again, a more complete (but still proof of concept) example can be found on Github here.
Approval Receipt
How you do this depends on the infrastructure you have setup. If you're using a Slack App for communication, you need to have a Webhook configured to receive interactive payloads from your end users.
For many organizations this may be hosted in AWS, but if you're lucky you get to use something lightweight and incredibly flexible like a Tines Webhook.
Smart Group Creation
Our Smart Group criteria is simple:
app.bundleId = [the bundle ID of our target application]
Simply put, this means we're only targeting systems on which the application of interest is already installed.
The below snippet assumes you're using the jamf_client.py helper module on Github, but its use isn't mandatory:
# Check if our Smart Group already exists - Create it if it doesn't
group_name = f"Patch Mgmt - {jai_info['app_name']}"
group_response = jamf_client.get_computer_group(name=group_name)
if 'Error' not in group_response:
group_id = group_response['computer_group']['id']
else:
group_config = helpers.parse_group_xml('./smart_group.xml', group_name,
jai_info["bundle_id"])
group_id = jamf_client.create_computer_group(group_config)['id']
Add & Configure
Finally, we'll create a new App Installer deployment according to criteria we've defined in advance. This logic can be found in 'payload' below and creates a deployment that is:
- Active (enabled: True)
- Added to a category we've previously created
- Scoped to the Smart Group we just created (or found, if it already existed)
- Going to install automatically (instead of Self Service)
- Set to notify the user of updates every 24 hours (notificationInterval: 24)
- Not going to force update (deadline: None)
- Set to install recommended configuration profiles
def deploy_app_installer(self, jai_name: str, jai_id: str,
smart_group_id: str, notification_interval: int = 24,
install_config_profiles: bool = True):
"""
Configure and deploy an App Installer.
"""
payload = {
"name": jai_name,
"enabled": True,
"appTitleId": jai_id,
"siteId": -1,
"categoryId": 12,
"smartGroupId": smart_group_id,
"deploymentType": "INSTALL_AUTOMATICALLY",
"notificationSettings": {
"notificationMessage": None,
"notificationInterval": notification_interval,
"deadlineMessage": None,
"deadline": None
},
"installPredefinedConfigProfiles": install_config_profiles
}
response = self.session.post(f'{self.base_url}/v1/app-installers/deployments',
json=payload)
if response.status_code == 200:
return response.json()
else:
print('App Installer deployment failed!')
return {'Error': 'App Installer deployment failed!'}
Maintain and Report
With any luck we now have one or more App Installers out there that are chugging right along. But like we touched on before - Things can occasionally go wrong and it would be unwise to not plan for failure.
So let's periodically check for failed installations and retry accordingly:
# Retrieve all deployed App Installers and parse their bundle ID's
deployed_jais = jamf_client.get_deployed_app_installers()
# Iterate over deployed App Installers
for deployment in deployed_jais:
deployment_details = jamf_client.get_deployment_details(deployment['id'])
# If failures are noted in a deployment, send a 'Retry All' command
if deployment_details['failed'] != 0:
jamf_client.retry_failed_deployment(deployment['id'])
For clarity, this is what jamf_client is doing under the hood:
def get_deployment_details(self, deployment_id: str):
"""
Retrieve details for a single App Installer deployment.
"""
deployment_uri = f'/v1/app-installers/deployments/{deployment_id}'
response = self.session.get(f'{self.base_url}{deployment_uri}')
if response.status_code == 200:
return response.json()
else:
print("Error retrieving deployments",
response.status_code,
response.content)
...
def retry_failed_deployment(self, deployment_id: str) -> bool:
"""
Retry failed App Installer deployments.
"""
retry_uri = f'/api/v1/app-installers/deployments/{deployment_id}/computers/installation-retry'
response = self.session.post(f'{self.base_url}{retry_uri}')
if response.status_code == 200:
return response.json()
else:
print("Error retrieving App Installers",
response.status_code,
response.content)
Again, if you need a more complete example, head over to the corresponding section over on Github.