Update rings are a simple idea. A small group of Macs gets each update first. A larger group follows after a few days. The rest of the fleet follows after that. Declarative device management (DDM) gives Jamf Pro the tools to do this, but the tools do not map to rings directly.
This post explains a design that does it with one blueprint, assigned to all Macs. The blueprint has several component blocks. Each block switches on or off from the Mac’s OS version and its group membership. A Mac therefore receives only the declarations for its own ring.
The design does three things:
- It sets a floor. A Mac below macOS 27.0 is updated to 27.0 at once.
- It runs three rings. A Mac on 27.0 or later follows the latest minor release. Its ring sets when the update becomes visible and when it is enforced.
- It can stop updates. A Mac in a hold group loses its enforcement, and a separate profile locks its Software Update pane.
The two DDM declarations#
The design uses two DDM declarations. Their behaviour together explains most of the structure.
| Declaration | What it controls |
|---|---|
| Software Update Settings | Deferrals, automatic download and install, notifications, and what standard users can do |
| Software Updates | Enforcement: the target version and the date and time of the forced install |
A ring needs three stages for each new minor release:
- Hidden. The update exists, but the Mac does not offer it.
- Visible. The user sees the update in System Settings and can install it at a time that suits them.
- Enforced. The Mac installs the update at the deadline.
The settings declaration gives you stage 1 with a minor update deferral. The enforcement declaration gives you stage 3. The problem is how the two interact.
IMPORTANT
A managed update declaration overrides a minor update deferral. As soon as the Mac has the enforcement declaration, the update shows in System Settings, even if the deferral has not ended. I saw this on a Ring 2 test Mac.
So one block that carries both declarations cannot give you a hidden stage. The enforcement declaration must stay off the Mac until the update is meant to be visible. That is why Ring 1 and Ring 2 each use two blocks.
How a device is evaluated#
Each time a Mac syncs, the blueprint’s block criteria decide which declarations apply. The hold group is checked first. Every other Mac is then routed by OS version, and then by ring.
- Hold group. A held Mac gets no enforcement. It can still keep its deferral (see The hold group).
- Below 27.0. The Mac gets the baseline block, which forces it up to 27.0.
- 27.0 or later. The Mac gets the blocks for its ring. Ring 0 has one block. Ring 1 and Ring 2 each have a settings block that always applies, and an enforce block that applies only when the Mac is in that ring’s Reveal group.
Block scope conditions#
The blocks never overlap. Each ring block needs OS 27.0 or later. The baseline block needs an OS lower than 27.0. A Mac can match one ring only, because a single extension attribute sets the ring. If you change the attribute value, the Mac moves to a different ring group, and the blueprint swaps its declarations at the next sync.
| Block | Activates when all of | Declarations |
|---|---|---|
| Latest OS - Ring 00 - Dev | OS version at least 27.0; member of macOS Update Ring 0; not a member of macOS Update Hold Group | Software Updates, Software Update Settings |
| Latest OS - Ring 01 - Pilot (settings) | OS version at least 27.0; member of macOS Update Ring 1 | Software Update Settings |
| Latest OS - Ring 01 - Pilot (enforce) | OS version at least 27.0; member of macOS Update Ring 1; member of macOS Update Reveal Day 7; not a member of macOS Update Hold Group | Software Updates |
| Latest OS - Ring 02 - Production (settings) | OS version at least 27.0; member of macOS Update Ring 2 | Software Update Settings |
| Latest OS - Ring 02 - Production (enforce) | OS version at least 27.0; member of macOS Update Ring 2; member of macOS Update Reveal Day 14; not a member of macOS Update Hold Group | Software Updates |
| Device Below macOS Baseline | OS version higher than 15; OS version lower than 27.0; not a member of macOS Update Hold Group | Software Updates, Software Update Settings |
Look at where the hold condition sits. It is on the enforce blocks and the baseline block only. The Ring 1 and Ring 2 settings blocks do not check the hold group, so a held Mac keeps its deferral.
Ring timings#
| Ring | Minor deferral | Reveal group | Enforced after release | Install time |
|---|---|---|---|---|
| Ring 0 (Dev) | none | not used | 3 days | 18:00 local |
| Ring 1 (Pilot) | 7 days | macOS Update Reveal Day 7 | 14 days | 18:00 local |
| Ring 2 (Production) | 14 days | macOS Update Reveal Day 14 | 21 days | 18:00 local |
Each enforcement uses the Latest OS version type, with Ignore major versions selected. A ring therefore follows minor releases only. It never moves a Mac to a new major version.
The enforcement days count from Apple’s release date, not from the day the Mac joins a Reveal group. This gives each ring a fixed timeline for every minor release:
| Ring | Hidden | Visible | Enforced |
|---|---|---|---|
| Ring 0 | never | days 0 to 3 | day 3 |
| Ring 1 | days 0 to 7 | days 7 to 14 | day 14 |
| Ring 2 | days 0 to 14 | days 14 to 21 | day 21 |
Ring 0 has no deferral, so its Macs see the update at once. Ring 1 and Ring 2 each give the user seven visible days before the deadline.
Settings shared by all blocks#
Every settings declaration in the blueprint uses the same values. Only the minor deferral changes between rings.
| Setting | Value |
|---|---|
| Standard users can install updates | Enabled |
| Notifications for scheduled updates | Enabled |
| Automatically download OS updates | Always |
| Automatically install OS updates | Allowed |
| Automatically install security updates | Always |
| Major deferral | 90 days |
| Background Security Improvement installation | Enabled |
| Background Security Improvement removal | Disabled |
| Beta updates and recommended cadence | Not configured |
Release age#
The Reveal groups need to know how old the current minor release is. Jamf Pro has no “days since release” criterion. An extension attribute supplies one, and two smart groups use it.
| Object | Definition |
|---|---|
| Extension attribute: macOS release age | Data type Integer, input type Script |
| Smart group: macOS Update Reveal Day 7 | macOS release age more than 6 |
| Smart group: macOS Update Reveal Day 14 | macOS release age more than 13 |
The script reads the SOFA feed. It finds the entry for the Mac’s own major version and reports how many days ago its latest release came out. For example, a Mac on 27.0 reports 9 when 27.2 shipped nine days ago.
Some details matter:
- “More than” is strict in Jamf Pro. “More than 6” means day 7 or later. “More than 13” means day 14 or later.
- The age resets. When a new minor release ships, the age goes back to 0. The Macs leave the Reveal groups, their enforce blocks switch off, and the cycle starts again for the new release.
- A failure is safe. If the script cannot read or parse the feed, it reports nothing. An empty value matches no Reveal group, so no Mac is forced early.
- Timing is approximate. The inventory interval and the DDM sync can move the window by a day or two.
NOTE
SOFA recommends that you host your own copy of the feed for production use. The script uses the public feed.
#!/bin/zsh# Jamf Pro extension attribute: macOS release age (days)## Setup in Jamf Pro: Data type = Integer, Input type = Script# Reports how many days ago the latest release of this Mac's own macOS major# version came out, using the SOFA feed (sofafeed.macadmins.io).# Example: a Mac on 27.0 while 27.2 shipped 9 days ago reports 9.## If the feed cannot be read or parsed it reports nothing, so the Mac matches# no Reveal group and nothing is forced early.
feeds=( "https://sofafeed.macadmins.io/v2/macos_data_feed.json" "https://sofafeed.macadmins.io/v1/macos_data_feed.json")
major=$(/usr/bin/sw_vers -productVersion | /usr/bin/cut -d. -f1)tmp=$(/usr/bin/mktemp -t macosage) || { echo "<result></result>"; exit 0; }trap '/bin/rm -f "$tmp"' EXIT
read -r -d '' jxa <<'JXA'function run(argv) { ObjC.import('Foundation'); var data = $.NSData.dataWithContentsOfFile(argv[0]); if (!data) { return ''; } var text = ObjC.unwrap($.NSString.alloc.initWithDataEncoding(data, $.NSUTF8StringEncoding)); var feed; try { feed = JSON.parse(text); } catch (e) { return ''; } var list = feed && feed.OSVersions; if (!list) { return ''; } var major = String(argv[1]); var entry = list.filter(function (v) { return String(v.OSVersion).trim().split(' ').pop() === major; })[0]; if (!entry || !entry.Latest || !entry.Latest.ReleaseDate) { return ''; } var released = new Date(entry.Latest.ReleaseDate); if (isNaN(released.getTime())) { return ''; } var days = Math.floor((Date.now() - released.getTime()) / 86400000); return String(Math.max(0, days));}JXA
days=""for url in "${feeds[@]}"; do if /usr/bin/curl -fsSL --max-time 30 -A "macos-release-age-ea" -o "$tmp" "$url"; then days=$(/usr/bin/osascript -l JavaScript -e "$jxa" "$tmp" "$major" 2>/dev/null) [[ -n "$days" ]] && break fidone
echo "<result>${days}</result>"The script tries the v2 feed first and the v1 feed second. It parses the JSON with JavaScript for Automation (JXA), so it needs no extra tools on the Mac.
The baseline block#
A new or rebuilt Mac can arrive on an older OS. The baseline block brings it up to 27.0 before the rings take over.
| Setting | Value |
|---|---|
| Enforcement type | Specific OS version and time |
| Target OS version | 27.0 |
| Date and time | 10/01/2026, 13:04 (as shown in Jamf; already passed) |
| Settings declaration | Major deferral 90 days, no minor deferral, other values as in the shared settings |
Two choices make this block work:
- The date is in the past. A deadline that has already passed tells the Mac to install now. The block therefore needs no date maintenance. A Mac built on macOS 26 was forced up to 27.0 by this block.
- The target is a specific version, not “latest”. With “latest”, a new Mac could go straight past the version its ring is on. A fixed baseline puts every Mac at the same start point, and the ring controls each step after that.
To raise the baseline later, change the target version (for example to 27.1) and the block’s OS version condition together. Keep the date in the past. Apple must still offer the target version, or the update cannot complete.
The hold group#
Sometimes you must stop an update, for example when a release breaks an app that a team depends on. A Mac in macOS Update Hold Group loses its enforce blocks and its baseline block. Any enforcement that has not started is cancelled.
Usually only Ring 1 and Ring 2 need a hold, because Ring 0 has already taken the update. The hold has these effects:
- The deferral stays. The settings blocks do not check the hold group, so a held Ring 1 or Ring 2 Mac keeps its deferral.
- The update can still become visible. After the deferral ends, nothing hides the update. A separate profile locks the Software Update pane for this reason.
- New Macs stop below the baseline. The baseline block also excludes held Macs. A held new or rebuilt Mac stays below 27.0 until you release it.
- A release can update Macs at once. When you remove Macs from the group, their enforce blocks return at the next sync. If the enforcement deadline has already passed, the Macs update immediately. Release them in batches.
WARNING
Automatic download is set to Always, so the update can already be on the Mac when you hold it. Hold early.
Update pane lock#
The pane lock is a normal configuration profile, separate from the blueprint. It is a Custom Settings payload for the com.apple.systempreferences domain. It is scoped to macOS Update Hold Group only, with no exclusions, and it installs automatically. When a Mac leaves the group, Jamf Pro removes the profile. I tested this on a test Mac.
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"><plist version="1.0"><dict> <key>DisabledSystemSettings</key> <array> <string>com.apple.Software-Update-Settings.extension</string> </array></dict></plist>DisabledSystemSettings takes a list of System Settings extension identifiers. This one removes access to the Software Update pane.
WARNING
The lock does not stop an admin who runs softwareupdate in Terminal. The lock also stops users installing security updates from System Settings, so keep holds short.
What is tested#
All parts of the design are tested on test Macs:
- Ring switching works. A change to the ring extension attribute swaps the declarations.
- The baseline block forces a Mac on macOS 26 up to 27.0.
- A managed update declaration overrides a minor deferral.
- The release age script reports the correct number of days against the live SOFA feed. The feed field names (
OSVersions,OSVersion,Latest.ReleaseDate) match the script. - Macs on the test networks can reach
sofafeed.macadmins.io. - Ring enforcement days count from Apple’s release date.
- Both Reveal groups populate on day 7 and day 14 of a real release.
- The enforce blocks leave a Mac after it joins the hold group, and Jamf Pro shows the change.
- The OS version criteria compare dotted versions as numbers. For example, 27.10 counts as later than 27.2.
Two behaviours are design choices that you may want to change:
- A Mac with no ring gets nothing. A Mac on 27.0 or later that is in no ring group matches no block, so it gets no update management. Give new Macs a default ring value to prevent this.
- The baseline block excludes held Macs. This keeps the hold absolute, but a held new Mac stays on an old OS. You may prefer to let the baseline apply to held Macs.