While Support App is often used to show buttons linking to organization resources such as apps and URLs, it also supports running more complex tasks using scripts. Organizations sometimes have specific use cases and can leverage Support App to use as a frontend, hide complexity and deliver a nice user experience around it. Some example workflows include doing a check-in with a Device Management Service, changing user privilege or collecting logs. Most of these tasks require the script to run with elevated privileges, which is called Privileged Scripts in Support App.
To separate privileges, Support App uses a Privileged Helper Tool, which is responsible for running the script as root while the main app runs within the user context. One of the challenges is to make sure the app runs the scripts securely without the risk of a standard user or bad actor tampering with the script. Before Support App 3.0, it already had built-in protections and checks for the right script owner and permissions (root:wheel and 755).
During the development cycle of Support App 3.0, an interesting feature request came in to support running scripts deployed using Apple’s Declarative Device Management (DDM). Due to the built-in checks, these scripts were denied to run as it has a different owner: _rmd. It made sense to me to fix this and support the most secure way to deploy scripts on macOS. While you can still deploy scripts with Support App 3.0 using any preferred method, I got some questions about how to use the DDM method together with Support App and I decided to write this article with technical guidance.
What are Service Background Tasks (DDM)
Service Background Tasks is a feature within Apple’s Declarative Device Management (DDM), which was introduced in 2024 with macOS 15 Sequoia. It can be used to securely deploy assets such as executables, scripts, configuration files and execute these using launchd configuration files like LaunchAgents/LaunchDaemons.
Some of the main benefits:
- Assets and launchd files are stored in tamper protected location:
/private/var/db/ManagedConfigurationFiles/BackgroundTaskServices - Owner and permissions are immutable
- Agentless: DDM takes care of everything
Ideally, all scripted and non-MDM methods of distributing assets and launchd files should be migrated to Service Background Tasks in DDM. The deployment will become standardized, MDM-independent and protected against tampering by users or bad actors. Even local administrators can’t touch it. So if that was a reason for organizations to let users not be local administrators, this can now be properly remediated.
I haven’t seen many implementations in production yet, but as MDM vendors are increasingly adding support for more DDM declaration types (such as Jamf with Blueprints), we may see more organizations adopting it.
The configuration itself is relatively easy, but may vary between Device Management Services depending on their implementation. Basically you need the following:
- A configuration with the type
com.apple.configuration.services.background-taskscontaining information about the task(s), launchd file(s), context (daemon or agent) and references to the assets. - An asset with the type
com.apple.asset.datacontaining information about where to find the asset(s), SHA-256 hash and some other data. - A (web) location the device can reach to get the asset (zipped) and launchd files
How to deploy scripts
I thought it was a nice use case to use Privileged Scripts in Support App with Service Background Tasks to increase security with Apple frameworks, but obviously the instructions below can be used for other apps or use cases as well.
The implementation for Support App only uses scripts and not launchd configuration files, so I’ll leave that last part out. For this demo, we’ll use the example use case where Support App displays the last Jamf Pro check-in in an Extension and run a check-in on-demand when clicked on it.
- Start by creating your script(s) you want to deploy. Copy and save the two scripts below
#!/bin/zsh --no-rcs
# Support App Extension - Jamf Pro Last Check-In Time
#
#
# Copyright 2025 Root3 B.V. All rights reserved.
#
# Support App Extension to get the Jamf Pro Last Check-In Time
#
# REQUIREMENTS:
# - Jamf Pro Binary
#
# THE SOFTWARE IS PROVIDED BY ROOT3 B.V. "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
# EVENT SHALL ROOT3 B.V. BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
# IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
# ------------------ edit the variables below this line ------------------
# Enable 24 hour clock format. 12 hour clock enabled by default
twenty_four_hour_format="true"
# Extension ID
extension_id="last_check_in"
# --------------------- do not edit below this line ----------------------
# Support App preference plist
preference_file_location="/Library/Preferences/nl.root3.support.plist"
# Start spinning indicator
defaults write "${preference_file_location}" "${extension_id}_loading" -bool true
# Replace value with placeholder while loading
defaults write "${preference_file_location}" "${extension_id}" -string "KeyPlaceholder"
# Keep loading effect active for 0.5 seconds
sleep 0.5
# Get last Jamf Pro check-in time from jamf.log
last_check_in_time=$(grep "Checking for policies triggered by \"recurring check-in\"" "/private/var/log/jamf.log" | tail -n 1 | awk '{ print $2,$3,$4 }')
# Convert last Jamf Pro check-in time to epoch
last_check_in_time_epoch=$(date -j -f "%b %d %T" "${last_check_in_time}" +"%s")
# Convert last Jamf Pro epoch to something easier to read
if [[ "${twenty_four_hour_format}" == "true" ]]; then
# Outputs 24 hour clock format
last_check_in_time_human_reable=$(date -r "${last_check_in_time_epoch}" "+%A %H:%M")
else
# Outputs 12 hour clock format
last_check_in_time_human_reable=$(date -r "${last_check_in_time_epoch}" "+%A %I:%M %p")
fi
# Write output to Support App preference plist
defaults write "${preference_file_location}" "${extension_id}" -string "${last_check_in_time_human_reable}"
# Stop spinning indicator
defaults write "${preference_file_location}" "${extension_id}_loading" -bool false#!/bin/zsh --no-rcs
# Support App Extension - Jamf Pro Check-In
#
#
# Copyright 2025 Root3 B.V. All rights reserved.
#
# Support App Extension to get the Jamf Pro Last Check-In Time
#
# REQUIREMENTS:
# - Jamf Pro Binary
#
# THE SOFTWARE IS PROVIDED BY ROOT3 B.V. "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
# EVENT SHALL ROOT3 B.V. BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
# IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
# ------------------ edit the variables below this line ------------------
# Enable 24 hour clock format. 12 hour clock enabled by default
twenty_four_hour_format="true"
# Extension ID
extension_id="last_check_in"
# --------------------- do not edit below this line ----------------------
# Support App preference plist
preference_file_location="/Library/Preferences/nl.root3.support.plist"
# Start spinning indicator
defaults write "${preference_file_location}" "${extension_id}_loading" -bool true
# Replace value with placeholder while loading
defaults write "${preference_file_location}" "${extension_id}" -string "Checking in..."
# Perform a Jamf Pro check-in
/usr/local/bin/jamf policy
# Run script to populate new values in Extension
/private/var/db/ManagedConfigurationFiles/BackgroundTaskServices/Services/nl.root3.support/jamf_last_check-in_time.zsh- Save both scripts to location such as
~/Downloads/jamf_check-in - Navigate to the folder in Terminal and make both scripts executable, as required by Apple:
“If ExecutableAssetReference is present, the POSIX permissions of the files in the zip archive need to be set correctly. For example, executables must have the “x” bit set.”
cd ~/Downloads/jamf_check-in
chmod +x jamf_last_check-in_time.zsh
chmod +x jamf_check-in.zsh- Compress both scripts into a zip file and call it jamf_check-in.zip:

- Go back to Terminal and run the following command to get the SHA-256 hash from the ZIP file. This is required so when macOS downloads the asset to the device, it can validate that it matches the exact file it is expecting. Copy this SHA-256 hash and save to put later in the asset declaration:
sha256sum ~/Downloads/jamf_check-in/jamf_last_check_in.zip
9733e5dfac9b3c49d4451eb11a3728d283c412ad44ad85dd32c2b4276e10b4bb- Now we need to upload the zip to a public web location the device can reach. In the end, it will be macOS initiating the download of the zip asset. In this example, I used a free Cloudflare R2 bucket to host the asset but there lots of other hosted options available.
- When signed in at dash.cloudflare.com, go to Storage & Databases > R2 object storage > Overview

- Click on Create bucket or choose any existing bucket:

- Add the zip asset to the bucket:

- Go to the bucket Settings to make sure the bucket and asset are publicly available. Be mindful with this as Cloudflare will charge if you reach the Operations limit.

- Go back to the bucket and click on the uploaded zip asset. You can now copy the public URL we need to enter later in the asset declaration:

- Next is preparing the declarations. I will provide an example for a Jamf Pro Blueprint and also the raw declaration files which you can use in another Device Management Service with support for Service Background Tasks (or custom declarations).
Creating Jamf Blueprint (option 1)
If we translate that to the implementation with Jamf Blueprints, both declarations are combined in the interface and just a few values are needed:
- First create a new Blueprint, give it a name and add the Service Background Tasks component:

- Remove the Launchd asset #1 component as we don’t need it for this use case:

- Fill in the details copied from earlier:
- Task type, such as
nl.root3.support.scripts - Optionally a description
- URL to the zip file on the Cloudflare R2 bucket
- SHA-256 hash
- Task type, such as

- Click Add and configure the Scope
- Finally, make sure to click on Deploy
Creating custom declarations (alternative)
Now the hardest options, using custom declarations. I could only try this with Jamf Pro and the custom declaration option in Blueprints and unfortunately it didn’t work, but here is the theory how it should work with other Device Management Services.
- Create is the
ServicesBackgroundTasksdeclaration:- The
Typemust becom.apple.configuration.services.background-tasks - The
Identifiershould be unique and theServerTokenis often handled by the MDM and represents the ‘version’ of the declaration. If this value changes, this will trigger the device to fetch the updated declaration. In many implementations, you are unlikely to be controlling this value. - This contains the
PayloadwithTaskType, a unique reverse DNS-style identifier, aTaskDescriptionandExecutableAssetReferencewhich specifies the asset declaration with the zip file.
- The
{
"Type": "com.apple.configuration.services.background-tasks",
"Identifier": "7416F6AA-A28E-4DA8-B858-7DB66797310E",
"ServerToken": "275B8B1C-A42E-460A-8D1A-D2EB2BF3A230",
"Payload": {
"TaskType": "nl.root3.support.scripts",
"TaskDescription": "Scripts for Root3 Support App",
"ExecutableAssetReference": "9FFBD027-5EFD-4DA2-8DCE-ED193187678D"
}
}- Second is the
AssetDatadeclaration:- It is very important that the
Identifiermatches theExecutableAssetReferencevalue from theServicesBackgroundTasksdeclaration, otherwise it won’t be able to associate the asset with the configuration. - The
Payloadcontains theDataURL, pointing to the zip file on the Cloudflare R2 bucket and theContentTypemust be set to “application/zip”. - We previously copied the SHA-256 hash and this value needs to be pasted in the
Hash-SHA-256field. - The
AuthenticationandTypemust be set to “MDM”
- It is very important that the
{
"Type": "com.apple.asset.data",
"Identifier": "9FFBD027-5EFD-4DA2-8DCE-ED193187678D",
"ServerToken": "4B41BBC5-9100-4000-9773-88A8DAE04915",
"Payload": {
"Reference": {
"DataURL": "https://REDACTED.r2.dev/jamf_last_check_in.zip",
"ContentType": "application/zip",
"Hash-SHA-256": "9733e5dfac9b3c49d4451eb11a3728d283c412ad44ad85dd32c2b4276e10b4bb"
}
}
}You should be able to copy and paste those declarations and modify to work with your Device Management Service. As mentioned before, I tried this using the Custom Declarations feature in Jamf Blueprints, but unfortunately I didn’t get the Asset to successfully deploy. According to the Jamf documentation, you should be able to reference an asset in the configuration using $PAYLOAD_# but for some reason it fails every time. The configuration with background task deploys just fine, as other configurations I’ve tried before. Even deploying just an asset seems to fail currently. As there are very limited examples out there, I’m unsure if I misconfigured the asset or that this is a bug in Jamf. If anybody figured it out, let me know!

Where scripts are located
When the deployment worked out, you should be able to validate in System Settings > General > Device Management > MDM Profile (or whatever it is called):

In the location below you can see how the scripts end up on the system, which is important as Support App will need to be told where the scripts are using the file paths:
- Script for the
Actionkey in the Extension:/private/var/db/ManagedConfigurationFiles/BackgroundTaskServices/Services/nl.root3.support.scripts/jamf_check-in.zsh - Script for the
OnAppearActionkey in the Extension:/private/var/db/ManagedConfigurationFiles/BackgroundTaskServices/Services/nl.root3.support.scripts/jamf_last_check-in_time.zsh

Configuring Support App
Next, we need to configure a Support App Extension which does the following:
- On appear, the Extension fetches the most recent Jamf Pro check-in by running the script
jamf_last_check-in_time.zsh. The result is then populated in the subtitle of the Extension - When clicking on the Extension, perform a Jamf Pro check-in for policies. This runs the
jamf_check-in.zshscript.
I’m using the Configurator Mode, which makes it very easy to configure this within the Support App user interface. As also noted in the UI, Privileged Scripts DO NOT run in Configurator Mode for security reasons and avoid (standard) users getting the ability to run any scripts with elevated privileges. The configuration first must be exported to a Configuration Profile and deployed using your Device Management Service.
- Let’s start by enabling Configurator Mode
- Add a new button and click on it to get into the details page:


- Change the Item type to Extension and enter the following details but feel free to modify however you like:
- Title: Check-in
- Action Type: Privileged Script
- Action:
/private/var/db/ManagedConfigurationFiles/BackgroundTaskServices/Services/nl.root3.support.scripts/jamf_check-in.zsh - SF Symbol: clock.badge.checkmark.fill
- Extension Identifier: last_check_in
- On appear action:
/private/var/db/ManagedConfigurationFiles/BackgroundTaskServices/Services/nl.root3.support.scripts/jamf_last_check-in_time.zsh

- Click Save > Done
- Click Export and choose the right format, depending on the options in your Device Management Service: Property List or Configuration Profile
- Upload the plist or profile and deploy!
- When everything worked out, the result should look like this:


Final thoughts
The question may arise, should I use Service Background Tasks with Support App? It depends. At the moment, it still requires quite some understanding of the framework, and the implementations across Device Management Services have some rough edges. In my opinion, to achieve wider adoption, they should provide a great admin experience around this, such as the ability to host assets, automatically calculating the SHA-256 hash, and without the need to fully understand what’s happening behind the scenes. For administrators who are trying to push the boundaries to adopt the latest Apple technology and benefit from the security improvements, it’s definitely worth at least testing it now.