Every IT department spends real time deciding what software is allowed on employee machines and what isn’t. The criteria are organization-specific and out of scope for this article. What’s in scope, however, is keeping an app that’s crossed the threshold from “allowed” to “actively deployed” up to date. It’s a dance every MacAdmin knows by heart.
The process usually starts with triaging which automation source supports the application, if any. Is it in Installomator? AutoPkg? Homebrew Cask? Vendor-provided PKG?
When the answer is “none of them”, you’re back to building the package by hand. Different vendors ship installers in different shapes and version updates require redoing the work.
This is where Installomator’s valuesfromarguments feature becomes a real game changer.
Going label-less
Installomator’s underlying functionality already handles installing almost any installer file. valuesfromarguments allows admins to leverage Installomator for “label-less” titles by passing the four required variables the script expects:
name— the application name as it appears in Applications, minus the.appextensiontype— the type of installer filedownloadURL— where the installer livesexpectedTeamID— the Apple Developer Team ID, for signature verification before installing
Three of those four are still vendor-website grunt work. The fourth, expectedTeamID, requires inspecting a signed binary. Either download the installer and run codesign -dvvv against the app inside, or use a tool like Apparency by Mothers Ruin Software.
I wanted to automate as much of this as possible. That’s where Patcher comes in.
The Patcher API solution
Patcher ships with a public catalog API at api.patcherctl.dev that stitches Installomator, Homebrew Cask, AutoPkg, and Jamf App Installer metadata into one queryable surface. It exposes a generate-label endpoint that does exactly what its name suggests: given an app slug, it returns a label-shaped object you can drop into Installomator’s valuesfromarguments flow. No authentication required.
Walking through ChatGPT Atlas
OpenAI shipped Atlas in October 2025, and at the time of writing it doesn’t have an Installomator label. It does have a Homebrew Cask entry, making it a perfect candidate to use as a demo.
Let’s make the API call to Patcher and see what comes back.
curl -s -X POST "https://api.patcherctl.dev/apps/chatgpt-atlas/generate-label" | jq
The response:
{
"label_name": "chatgpt-atlas",
"content": {
"name": "ChatGPT Atlas",
"type": "dmg",
"downloadURL": "https://persistent.oaistatic.com/atlas/public/ChatGPT_Atlas_Desktop_public_1.2026.119.1_20260504231115000.dmg",
"appNewVersion": "1.2026.119.1,20260504231115000"
},
"sources_used": ["homebrew_cask"],
"warnings": [
"expectedTeamID unknown — Installomator requires this for code-sign verification. Determine manually via `codesign -dvvv <path-to-app>` before deploying."
]
}
We can see from the response that three of the four required fields came back automatically (name, type, downloadURL). The endpoint also surfaced an appNewVersion, which you could use for version-targeting if needed.
The fourth, expectedTeamID, isn’t in the Cask metadata, so the endpoint tells you exactly what to run next. My personal preference is Apparency, but for the sake of demonstration we will download the DMG, mount it, and codesign the app inside.
$ hdiutil attach ~/Downloads/ChatGPT_Atlas_Desktop_public_1.2026.119.1_20260504231115000.dmg
...
/dev/disk4s1 Apple_HFS /Volumes/ChatGPT Atlas Installer
$ codesign -dvvv "/Volumes/ChatGPT Atlas Installer/ChatGPT Atlas.app" 2>&1 | grep TeamIdentifier
TeamIdentifier=2DC432GLL2hdiutil: Mount the installer DMG and note the volume name; codesign: Inspect the app’s Team IDLooking it up programmatically
If you’re deploying via Python, the same lookup is one HTTP request.
"""Resolve the four Installomator valuesfromarguments fields for a given app slug."""
import sys
import requests
def generate_label(slug: str) -> dict:
"""Hit the Patcher API and return the generated label."""
response = requests.post(f"https://api.patcherctl.dev/apps/{slug}/generate-label")
response.raise_for_status()
return response.json()
def main(slug: str) -> None:
result = generate_label(slug)
content = result["content"]
print(f"name={content['name']}")
print(f"type={content['type']}")
print(f"downloadURL={content['downloadURL']}")
for warning in result.get("warnings", []):
print(f"# {warning}")
if __name__ == "__main__":
main(sys.argv[1])
Save the script and give it a descriptive name. Execute the script using chatgpt-atlas as our slug name to see the output:
$ python3 generate_label.py chatgpt-atlas
...
name=ChatGPT Atlas
type=dmg
downloadURL=https://persistent.oaistatic.com/atlas/public/ChatGPT_Atlas_Desktop_public_1.2026.119.1_20260504231115000.dmg
# expectedTeamID unknown — Installomator requires this for code-sign verification...
Deploying via Terraform
Assuming you’ve already declared the Installomator script as a jamfpro_script resource, the policy resource references that script by ID and passes the four field values as numbered parameters.
resource "jamfpro_policy" "chatgpt_atlas_install" {
name = "Install ChatGPT Atlas"
enabled = true
trigger_other = "USER_INITIATED"
frequency = "Ongoing"
target_drive = "/"
category_id = jamfpro_category.standard_issue.id
site_id = -1
scope {
all_computers = true
}
self_service {
use_for_self_service = true
self_service_display_name = "ChatGPT Atlas"
install_button_text = "Install"
reinstall_button_text = "Reinstall"
self_service_description = "Install or update OpenAI's ChatGPT Atlas browser."
self_service_category {
id = jamfpro_category.standard_issue.id
display_in = true
feature_in = false
}
}
payloads {
scripts {
id = jamfpro_script.installomator.id
priority = "After"
parameter4 = "valuesfromarguments"
parameter5 = "name=ChatGPT Atlas"
parameter6 = "type=dmg"
parameter7 = "downloadURL=https://persistent.oaistatic.com/atlas/public/ChatGPT_Atlas_Desktop_public_1.2026.119.1_20260504231115000.dmg"
parameter8 = "expectedTeamID=4P9YKVZSXJ"
}
}
}valuesfromarguments)For ad-hoc deploys, parameter 4 is set to valuesfromarguments instead of the label name. Parameters 5 through 11 carry the field-value pairs Installomator expects.
Deploying via Python and the Classic API
For shops not using Terraform, policies can be created using Jamf’s Classic API. The modern Jamf Pro API hasn’t shipped policy CRUD endpoints yet, so the script below POSTs XML to /JSSResource/policies/id/0 instead.
"""Deploy ChatGPT Atlas via Installomator as a Jamf Pro policy."""
import xml.etree.ElementTree as ET
import requests
JAMF_URL = "https://yourorg.jamfcloud.com"
CLIENT_ID = "your-client-id"
CLIENT_SECRET = "your-client-secret"
INSTALLOMATOR_SCRIPT_ID = "123" # ID of your uploaded Installomator script
CATEGORY_ID = "10"
ATLAS_DOWNLOAD_URL = (
"https://persistent.oaistatic.com/atlas/public/"
"ChatGPT_Atlas_Desktop_public_1.2026.119.1_20260504231115000.dmg"
)
def get_bearer_token() -> str:
"""OAuth client_credentials grant against the Jamf Pro API."""
response = requests.post(
f"{JAMF_URL}/api/oauth/token",
data={
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
"grant_type": "client_credentials",
},
)
response.raise_for_status()
return response.json()["access_token"]
def build_policy_xml() -> bytes:
"""Construct the Classic API policy XML payload."""
policy = ET.Element("policy")
general = ET.SubElement(policy, "general")
ET.SubElement(general, "name").text = "Install ChatGPT Atlas"
ET.SubElement(general, "enabled").text = "true"
category = ET.SubElement(general, "category")
ET.SubElement(category, "id").text = CATEGORY_ID
ET.SubElement(general, "trigger_other").text = "USER_INITIATED"
ET.SubElement(general, "frequency").text = "Ongoing"
self_service = ET.SubElement(policy, "self_service")
ET.SubElement(self_service, "use_for_self_service").text = "true"
ET.SubElement(self_service, "self_service_display_name").text = "ChatGPT Atlas"
ET.SubElement(self_service, "install_button_text").text = "Install"
ET.SubElement(self_service, "reinstall_button_text").text = "Reinstall"
scripts = ET.SubElement(policy, "scripts")
ET.SubElement(scripts, "size").text = "1"
script = ET.SubElement(scripts, "script")
ET.SubElement(script, "id").text = INSTALLOMATOR_SCRIPT_ID
ET.SubElement(script, "priority").text = "After"
ET.SubElement(script, "parameter4").text = "valuesfromarguments"
ET.SubElement(script, "parameter5").text = "name=ChatGPT Atlas"
ET.SubElement(script, "parameter6").text = "type=dmg"
ET.SubElement(script, "parameter7").text = f"downloadURL={ATLAS_DOWNLOAD_URL}"
ET.SubElement(script, "parameter8").text = "expectedTeamID=2DC432GLL2"
return ET.tostring(policy, encoding="utf-8", xml_declaration=True)
def create_policy(token: str, xml_body: bytes) -> None:
"""POST the policy XML to the Classic API. ID 0 means 'new'."""
response = requests.post(
f"{JAMF_URL}/JSSResource/policies/id/0",
data=xml_body,
headers={
"Authorization": f"Bearer {token}",
"Content-Type": "application/xml",
"Accept": "application/xml",
},
)
response.raise_for_status()
print(f"Created with status: {response.status_code}")
if __name__ == "__main__":
token = get_bearer_token()
create_policy(token, build_policy_xml())
Bonus: lookup from Claude Code
If Claude Code is already part of your workflow, Patcher ships a skill at .claude/skills/patcher/ that answers the “what coverage exists for this app?” question without leaving the chat session:
/patcher chatgpt-atlas
The skill checks Installomator, Homebrew Cask, AutoPkg, and Jamf App Installer in parallel and prints a coverage report per source. It’s a faster way to confirm “yes, Cask has it, Installomator doesn’t” before reaching for the generate-label endpoint.
Project-scoped install is automatic if you clone Patcher: open any Claude Code session from the repo root and the skill loads itself. For a user-scoped install that’s available across every project on your machine, see the skill documentation.
Wrap-up
That’s the dance, condensed:
curl https://api.patcherctl.dev/apps/<slug>/generate-labelfor the three resolvable fieldscodesign -dvvv(or Apparency) against the signed binary forexpectedTeamIDterraform apply, or POST the Classic API XML
ChatGPT Atlas was today’s demo, but the same recipe works for any app that has a Cask entry and no Installomator label yet. That’s most of the long tail.
The Patcher API is public and unauthenticated, so the lookup step works from anywhere. Endpoint reference and full schema live in project docs. The OpenAPI schema and Swagger UI are also available if you’d rather poke at the surface directly.
If you find an app where generate-label comes up short, or you want to contribute a resolver for fields beyond what Cask provides, open an issue on the Patcher repo or find me in the #patcher channel on MacAdmins Slack. The catalog gets better the more apps it covers, and the most useful contributions are usually “I tried this against $vendor and here’s what came back garbled.”