-
Notifications
You must be signed in to change notification settings - Fork 4
Pro Workflows
Copy-paste recipes for Jamf Pro administration. For flag details, see Jamf Pro Commands. For shared patterns (apply, scaffold, pagination), see CLI Patterns. For output format options, see Output Formats.
If you run these against a platform gateway profile, a handful are refused before a request is sent (exit code 8), the MDM device actions above all. See Jamf Pro Commands#Gateway Profiles: Refused Commands and Platform API GA Migration.
Recipes written against v1.28.0 or earlier may print a warning. 103
proresource spellings were retired in v1.29.0. 100 of them still run, after a line on stderr naming the replacement:warning: `computers-inventory` is a deprecated name for `computer-inventory` and stops working after 2027-03-09. Use `pro computer-inventory`.Neither
--quietnor--no-hintssilences it, so a job that folds stderr into a log will collect these lines. Short aliases (pro computers,pro comp,pro md) are permanent and print no warning. Full tables: Pro Command Renames.
# Full instance dashboard: version, inventory counts, feature flags, cert expiry, alerts
jamf-cli pro overview
# Machine-readable overview for alerting pipelines
jamf-cli pro overview -o json
# Quick reachability check across all configured profiles
jamf-cli config list --status# List all computers (auto-paginates by default)
jamf-cli pro computers list
# Export full computer inventory to CSV
jamf-cli pro computers list -o csv --out-file computers.csv
# Export mobile devices to CSV
jamf-cli pro mobile-devices list -o csv --out-file mobile-devices.csv
# Extract just serial numbers (one per line)
jamf-cli pro computers list --field serialNumber
# Multi-field extraction via jq
jamf-cli pro computers list -o json \
| jq -r '.[] | [.id, .name, .serialNumber] | @csv' > inventory.csv
# Single page only (disable auto-pagination)
jamf-cli pro computers list --all=false --page-size 50
# Wide table with all columns
jamf-cli pro computers list -o table -w# Computers that haven't checked in for 30+ days
# Epoch comparison works in any locale and date format
jamf-cli pro computers list -o json | jq --argjson cutoff "$(date -v-30d +%s)" '
[ .[]
| select(.lastContactTime != null)
| select((.lastContactTime | sub("\\.[0-9]+.*";"") | strptime("%Y-%m-%dT%H:%M:%S") | mktime) < $cutoff)
]'
# Just the count
jamf-cli pro computers list -o json | jq --argjson cutoff "$(date -v-30d +%s)" '
[ .[]
| select(.lastContactTime != null)
| select((.lastContactTime | sub("\\.[0-9]+.*";"") | strptime("%Y-%m-%dT%H:%M:%S") | mktime) < $cutoff)
] | length'
# Stale device names only
jamf-cli pro computers list -o json | jq -r --argjson cutoff "$(date -v-30d +%s)" '
.[]
| select(.lastContactTime != null)
| select((.lastContactTime | sub("\\.[0-9]+.*";"") | strptime("%Y-%m-%dT%H:%M:%S") | mktime) < $cutoff)
| .name'
# Linux-compatible cutoff (replace -v-30d with GNU date)
# cutoff=$(date -d '30 days ago' +%s)# List all Classic API policies
jamf-cli pro classic-policies list
# Count policies
jamf-cli pro classic-policies list -o json | jq length
# List macOS configuration profiles
jamf-cli pro classic-macos-config-profiles list
# List mobile device configuration profiles
jamf-cli pro classic-mobile-config-profiles list
# Count all config management objects at once
echo "Policies: $(jamf-cli pro classic-policies list -o json --no-input | jq length)"
echo "macOS Profiles: $(jamf-cli pro classic-macos-config-profiles list -o json --no-input | jq length)"
echo "Mobile Profiles: $(jamf-cli pro classic-mobile-config-profiles list -o json --no-input | jq length)"
echo "Scripts: $(jamf-cli pro scripts list -o json --no-input | jq length)"
echo "Packages: $(jamf-cli pro packages list -o json --no-input | jq length)"
# Find policies by name pattern
jamf-cli pro classic-policies list -o json | jq '.[] | select(.name | test("patch"; "i"))'
# Export a specific policy's XML for review
jamf-cli pro classic-policies get --name "Install Chrome"View and modify scope on policies, configuration profiles, and other Classic API resources without editing raw XML. Scope is a subcommand on each resource that supports it.
Changed in v1.30.0:
scopetakes an id positionally, and a name through--name.scope get "Deploy Chrome"now exits 2 naming--name, because a non-numeric positional is no longer sent to the API as an id. The two together are refused, matching every other Classic command. Each command also registers only the categories its own resource carries, so a cross-family flag (--mobile-device-grouponclassic-macos-config-profiles,--ibeacononclassic-mac-apps) is now cobra's unknown flag, and the error names the categories that resource does accept.--ibeacon(policies, macOS and mobile config profiles) and--class(ebooks, target only) are writable for the first time. Per-resource matrix: Jamf Pro Commands.
# Check what's in scope for a policy, by id or by name
jamf-cli pro classic-policies scope get 1
jamf-cli pro classic-policies scope get --name "Deploy Chrome" -o tableRESULTS (4 total)
SECTION TYPE NAME
───────────────────────────────────────────
target all_computers true
target computer_group All Managed Clients
limitation network_segment Corporate
exclusion computer_group Test Machines
# Add a computer group to targets, by policy id
jamf-cli pro classic-policies scope add 1 --computer-group "Marketing Macs"
# Add a building to exclusions, by policy name
jamf-cli pro classic-policies scope add --name "Deploy Chrome" --section exclusion --building "London"
# Add a directory user group to limitations
jamf-cli pro classic-policies scope add --name "Deploy Chrome" --section limitation --user-group "Staff"
# Preview the write first: -n prints method, path and the <scope> body to stderr and sends nothing
jamf-cli pro classic-policies scope add --name "Deploy Chrome" --section limitation --ibeacon "Reception" -n
# Scope a macOS config profile
jamf-cli pro classic-macos-config-profiles scope add --name "Wi-Fi Settings" --computer-group "All Managed Clients"
jamf-cli pro classic-macos-config-profiles scope add --name "Wi-Fi Settings" --section exclusion --department "IT"Each category is valid in only some sections, and --help says which: --building, --computer, --computer-group, --department, --jss-user, --jss-user-group, --mobile-device and --mobile-device-group are target or exclusion; --ibeacon, --network-segment, --user and --user-group are limitation or exclusion. --user is a directory or local username, free text and not a Jamf Pro user; --jss-user is the Jamf Pro account.
# Remove a test group from targets
jamf-cli pro classic-policies scope remove --name "Deploy Chrome" --computer-group "Test Group"
# Remove a building from exclusions, by id
jamf-cli pro classic-policies scope remove 1 --section exclusion --building "London"# Add the same exclusion to multiple policies
for policy in "Deploy Chrome" "Deploy Firefox" "Deploy Slack"; do
jamf-cli pro classic-policies scope add --name "$policy" \
--section exclusion --computer-group "Test Machines"
done
# Audit scope across policies: export as JSON for processing
for policy in "Deploy Chrome" "Install Updates" "Security Baseline"; do
echo "=== $policy ==="
jamf-cli pro classic-policies scope get --name "$policy" -o json
doneA loop keyed on ids is the safer one: Classic names are not unique, and a --name matching more than one record is now refused rather than resolved to the first match in document order. Feed the loop jamf-cli pro classic-policies list --field id where the names are not under your control.
Note: add and remove are idempotent. Adding a group that is already scoped, or removing one that is not, changes nothing, so both are safe in loops. The request body carries nothing but
<scope>, so no other part of the object is rewritten: a 19 KB profile's payloads come back byte-identical.<scope>itself is replaced whole, which is why the current scope is read first. After the write the CLI re-reads the scope and diffs everything it sent against what came back, naming anything the server dropped rather than checking only the category you touched.
pro open launches the Jamf Pro web interface at a named section, so a CLI edit can be checked in the UI without assembling the URL by hand. 139 sections are named; --list prints them, and tab completion offers each with the heading the interface uses.
# Change scope, then open the policy list and look at it
jamf-cli pro classic-policies scope add --name "Deploy Chrome" --computer-group "Marketing Macs"
jamf-cli pro open policies
# Paste a link into a ticket or a Slack message instead of opening it
jamf-cli pro open macos-configuration-profiles --field url
jamf-cli pro open computers | pbcopy
# Every section name
jamf-cli pro open --listThe URL is printed rather than launched under --print, --no-input, --dry-run, --out-file, --field, --select, or whenever stdout is not a terminal, which is why the | pbcopy above needs no flag. A record-detail or wizard page carries no section name, so pass a path instead: jamf-cli pro open computers.html?id=42. Section names, the URL-resolution ladder and the other products' open are on Jamf Pro Commands.
apply is a name-based upsert: it creates the resource if it doesn't exist and replaces it (with confirmation) if it does. For flag details and collision handling see CLI Patterns#Apply (Upsert).
# Apply from stdin or file: creates or replaces by name
echo '{"name":"HQ","city":"Cupertino"}' | jamf-cli pro buildings apply --yes
jamf-cli pro buildings apply --from-file building.json --yes
# Classic API: same pattern with XML
cat policy.xml | jamf-cli pro classic-policies apply --yes
# Dry-run to preview without executing
jamf-cli pro buildings apply --from-file building.json --dry-runClassic create, update and apply take --set (which builds the whole body) or --from-file. The two are mutually exclusive. A Classic PUT is a partial update, so --set alone is a valid edit, with no fetch-merge cycle needed.
# Create a network segment straight from flags
jamf-cli pro classic-network-segments create \
--set name="Amsterdam Office" \
--set starting_address=10.1.1.1 \
--set ending_address=10.1.1.254
# Rename one and change nothing else: omitted fields keep their values
jamf-cli pro classic-network-segments update --name "Amsterdam Office" --set name="AMS Office"
# Learn the shape first: required fields, sections, enums, credential fields
jamf-cli pro classic-policies create --help
jamf-cli pro classic-policies create --scaffold > policy.xml
# ... delete the sections you do not need: the scaffold's specimen references
# point at objects that do not exist on your instance ...
jamf-cli pro classic-policies create --from-file policy.xml--set refuses an unknown field, an out-of-enum value, an empty value for an enum field, and any credential field. The server refuses none of those: the Classic API answers 201 for an unknown element and drops it, and 201 for an out-of-enum value, which reads back as the field's default. Put secrets in --from-file, never in a flag value.
# Safe to re-run: creates when missing, replaces when present
for name in "Security" "Productivity" "Development" "Utilities"; do
echo "{\"name\":\"$name\",\"priority\":0}" | jamf-cli pro categories apply --yes
done# Export, modify a field, re-apply
jamf-cli pro buildings get --name "HQ" -o json \
| jq '.city = "San Francisco"' \
| jamf-cli pro buildings apply --yes
# Clone with a new name
jamf-cli pro buildings get 1 -o json \
| jq '.name = "Branch Office Copy"' \
| jamf-cli pro buildings apply
# Sync a resource from prod to staging
jamf-cli pro buildings get --name "HQ" -p prod -o json \
| jamf-cli pro buildings apply -p staging --yesjamf-cli pro buildings delete --name "Old Office" --yes
jamf-cli pro classic-policies delete --name "Legacy Policy" --yes
jamf-cli pro classic-printers delete --name "Broken Printer" --dry-runpatch changes only the fields you specify, leaving everything else untouched. Use it to update a field on an existing resource without touching the rest of its configuration. patch is only available on resources whose API exposes an HTTP PATCH endpoint (e.g. computers, mobile-devices, blueprints, groups, settings singletons); resources without one (e.g. buildings, scripts) use update or apply instead.
# Update a single field by name
jamf-cli pro computers patch --name "Neil's MacBook" --set general.assetTag=CORP-101
# Update a computer by serial number
jamf-cli pro computers patch --serial C02X1234 --set general.assetTag=CORP-101
# Update multiple fields at once
jamf-cli pro computers patch --serial C02X1234 \
--set general.managed=true \
--set general.assetTag=CORP-101
# Preview the patchable fields for a resource
jamf-cli pro computers patch --scaffold
# Patch from a JSON merge-patch file
jamf-cli pro computers patch 101 --from-file changes.jsonUse --scaffold to see the full list of patchable fields. The API touches only the keys you send.
Preview a destructive operation with --dry-run before you run it.
# Delete scripts by ID: dry-run first
jamf-cli pro scripts list --field id | while read id; do
jamf-cli pro scripts delete "$id" --dry-run
done
# Execute deletes (--yes skips per-item confirmation)
jamf-cli pro scripts list --field id | while read id; do
jamf-cli pro scripts delete "$id" --yes
done
# Or delete by name, with no ID lookup
jamf-cli pro scripts delete --name "Old Script" --yes
# Scaffold a building JSON template, edit it, then apply
jamf-cli pro buildings apply --scaffold > building.json
# ... edit building.json ...
cat building.json | jamf-cli pro buildings apply --yes
# Clone a resource: get, modify, apply with a new name
jamf-cli pro buildings get 1 -o json | jq '.name = "Branch Office Copy"' | jamf-cli pro buildings applyAll generated delete commands accept --from-file (a file of IDs/names/serials) or --group (computers and mobile devices), and so do the destructive actions: erase, unmanage and the rest. Each action sends its own HTTP method on the bulk path, so a POST-only endpoint such as /v2/mobile-device-groups/{id}/erase gets a POST.
# Build a list of decommissioned computers and delete them all
jamf-cli pro computer-inventory delete --from-file decommissioned.txt --dry-run
jamf-cli pro computer-inventory delete --from-file decommissioned.txt --yes
# Delete all members of a computer group
jamf-cli pro computer-inventory delete --group "Decommissioned Macs" --yes
# Delete retired mobile devices by group
jamf-cli pro mobile-devices delete --group "Retired iPads" --yes
# Bulk delete stale config profiles by name
jamf-cli pro classic-macos-config-profiles list -o json \
| jq -r '.[] | select(.name | test("^LEGACY")) | .name' \
> legacy-profiles.txt
jamf-cli pro classic-macos-config-profiles delete --from-file legacy-profiles.txt --dry-run
jamf-cli pro classic-macos-config-profiles delete --from-file legacy-profiles.txt --yesFile format accepts IDs, names, serial numbers, UDIDs, or any supported lookup identifier, one per line, with # comments ignored:
# decommissioned.txt
C02X1234
C02Y5678
42
Mirror JCDS to a local file-share distribution point using packages sync. Runs unattended; SHA3-512 checksums detect remote updates so only changed files are re-downloaded.
# One-shot sync (safe: no deletion)
jamf-cli pro packages sync --dir /Volumes/Packages
# Full mirror: download new/changed files and delete orphans
jamf-cli pro packages sync --dir /Volumes/Packages --delete
# Preview before committing
jamf-cli pro packages sync --dir /Volumes/Packages --delete --dry-run \
| jq '.summary'Launch daemons run without a user session, so Keychain is unavailable. Store the client secret in a root-only readable file instead, and reference it with file: in the config profile.
# 1. Store the client secret in a file only root can read
sudo mkdir -p /private/etc/jamf-cli
printf '%s' 'your-client-secret' | sudo tee /private/etc/jamf-cli/jcds-sync-secret > /dev/null
sudo chmod 600 /private/etc/jamf-cli/jcds-sync-secret
# 2. Create the profile as root (prompts for client ID and secret)
sudo jamf-cli config add-profile jcds-sync \
--url https://your-instance.jamfcloud.com --auth-method oauth2After setup, edit root's config (sudo $EDITOR /var/root/.config/jamf-cli/config.yaml) and replace the generated keychain:… secret value with the file reference:
client-secret: "file:/private/etc/jamf-cli/jcds-sync-secret"Then reference the profile with -p in the launch daemon, which keeps credentials out of the plist:
<!-- /Library/LaunchDaemons/com.example.jcds-sync.plist -->
<?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>Label</key>
<string>com.example.jcds-sync</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/jamf-cli</string>
<string>-p</string>
<string>jcds-sync</string>
<string>pro</string>
<string>packages</string>
<string>sync</string>
<string>--dir</string>
<string>/Volumes/Packages</string>
<string>--delete</string>
<string>--no-input</string>
<string>--no-color</string>
</array>
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key>
<integer>2</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
<key>StandardOutPath</key>
<string>/var/log/jcds-sync.log</string>
<key>StandardErrorPath</key>
<string>/var/log/jcds-sync.log</string>
</dict>
</plist>Same file: secret approach, under a dedicated service account.
Headless servers have no D-Bus keychain daemon, so write the config file by hand; config add-profile would try to store the credentials in a keychain that does not exist.
# Create service account with home directory
sudo useradd -r -m -s /usr/sbin/nologin jcds-sync
# Store client secret, readable by the service account alone
sudo mkdir -p /etc/jamf-cli
printf '%s' 'your-client-secret' | sudo tee /etc/jamf-cli/jcds-sync-secret > /dev/null
sudo chmod 600 /etc/jamf-cli/jcds-sync-secret
sudo chown jcds-sync:jcds-sync /etc/jamf-cli/jcds-sync-secret
# Write the config by hand: client-id is no secret, client-secret uses a file: ref
sudo -u jcds-sync mkdir -p /home/jcds-sync/.config/jamf-cli
sudo tee /home/jcds-sync/.config/jamf-cli/config.yaml > /dev/null <<'EOF'
default-profile: jcds-sync
profiles:
jcds-sync:
url: https://your-instance.jamfcloud.com
auth-method: oauth2
client-id: your-client-id
client-secret: "file:/etc/jamf-cli/jcds-sync-secret"
EOF
sudo chmod 600 /home/jcds-sync/.config/jamf-cli/config.yaml
sudo chown jcds-sync:jcds-sync /home/jcds-sync/.config/jamf-cli/config.yaml/etc/systemd/system/jcds-sync.service:
[Unit]
Description=JCDS package sync
After=network-online.target
Wants=network-online.target
ConditionPathIsMountPoint=/mnt/packages
[Service]
Type=oneshot
User=jcds-sync
ExecStart=/usr/local/bin/jamf-cli -p jcds-sync pro packages sync \
--dir /mnt/packages --delete --no-input --no-color/etc/systemd/system/jcds-sync.timer:
[Unit]
Description=Run JCDS sync daily at 02:00
[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true
[Install]
WantedBy=timers.targetsudo systemctl daemon-reload
sudo systemctl enable --now jcds-sync.timer
# View logs
journalctl -u jcds-syncNotable differences from launchd: ConditionPathIsMountPoint= skips the run if the volume isn't mounted (launchd has no equivalent); Persistent=true catches up on missed runs if the server was down at 02:00; logs go to journald with no log file to rotate.
result=$(jamf-cli pro packages sync --dir /Volumes/Packages --no-input --no-color)
failed=$(echo "$result" | jq '.summary.failed')
if [ "$failed" -gt 0 ]; then
echo "JCDS sync: $failed file(s) failed" >&2
echo "$result" | jq '[.files[] | select(.status == "failed")]' >&2
exit 1
fiTarget devices by serial number, name, ID, group, or file. Destructive operations have safety gates.
On a gateway profile, most MDM actions are refused (
lock,restart,shutdown, remote desktop and recovery lock on computers, and sixteenpro mobile-devicesactions) becausePOST /v2/mdm/commandsis not in the gateway's published API.blank-push,redeploy-framework,ddm-sync,renew-mdm,erase,remove-mdm/unmanageandflush-commandsstill work. Keep anoauth2profile against the instance for the rest, and select it with-p. Full table: Jamf Pro Commands#Device Actions.
# Send a blank push to trigger check-in
jamf-cli pro computers blank-push --serial C02X1234
# Redeploy the Jamf management framework
jamf-cli pro computers redeploy-framework --name "Neil's MacBook"
# Force a DDM sync
jamf-cli pro computers ddm-sync --id 42
# Renew MDM profile
jamf-cli pro computers renew-mdm --serial C02X1234
# Erase a computer (requires --yes)
jamf-cli pro computers erase --serial C02X1234 --yes
# Unmanage a mobile device (requires --yes)
jamf-cli pro mobile-devices unmanage --serial F4K3SER1AL --yesflush-commands clears stuck or failed MDM commands off a device, with no wipe.
# Flush failed commands from a computer (safe default)
jamf-cli pro computers flush-commands --serial C02X1234 --yes
# Flush both pending and failed (clears the whole queue: use with care)
jamf-cli pro computers flush-commands --serial C02X1234 --status both --yes
# Flush failed commands from an entire group (one API call)
jamf-cli pro computers flush-commands --group "Problem Devices" --yes
# Dry-run to preview what would be flushed
jamf-cli pro computers flush-commands --serial C02X1234 --dry-run# Blank push to all members of a group
jamf-cli pro computers blank-push --group "All Macs" --yes
# Redeploy framework on all listed devices from a file
jamf-cli pro computers redeploy-framework --from-file devices.txt --yes
# Destructive bulk operations require both --yes and --confirm-destructive
jamf-cli pro computers erase --group "Decommissioned" --dry-run
jamf-cli pro computers erase --group "Decommissioned" --yes --confirm-destructiveThe four commands above are hand-written and word their own previews ([dry-run] Would erase computer "FVFC41HCLYWP" (serial: …, id: 5)). A generated destructive action quotes its own name: a plain DELETE reads This will delete …, and every other operation is named in the prompt.
$ jamf-cli pro mobile-device-groups erase 67
⚠️ This will run "erase" on mobile-device-group 67. Type 'yes' to confirm:
$ jamf-cli pro mobile-device-groups erase --from-file groups.txt
⚠️ This will run "erase" on 2 mobile-device-groups. Type 'yes' to confirm:
--dry-run follows the same rule, and so does the --from-file flag's help text (Path to file listing IDs or names to run "erase" on):
$ jamf-cli pro mobile-device-groups erase --from-file groups.txt --dry-run
[dry-run] Would run "erase" on mobile-device-group "67" (id: 67)
[dry-run] Would run "erase" on mobile-device-group "66" (id: 66)
# By serial number, Jamf ID, or name
jamf-cli pro device C02X1234
jamf-cli pro device 42
jamf-cli pro device "Neil's MacBook" -o jsonShows identity, hardware, OS, security posture (FileVault, SIP, Gatekeeper, firewall), user info, MDM command history, and policy execution logs. The CLI fetches the data in parallel; partial failures appear as stderr warnings.
# Fleet security posture
jamf-cli pro report security -o table
# Machine-readable for alerting
jamf-cli pro report security -o jsonclassic-computer-app-usage reports which apps ran on one Mac over a date range, for licence reclaim and for checking whether a deployed app is used.
# Last 30 days for one machine, as a spreadsheet
jamf-cli pro classic-computer-app-usage --serial C02XL0ABCDEF --last 30d \
-o csv --out-file app-usage.csv
# Top 10 apps by foreground minutes last month
# (rows are flat, one per date + app, so jq needs no unnesting)
jamf-cli pro classic-computer-app-usage --id 42 --start 2026-06-01 --end 2026-06-30 \
-o json --no-input \
| jq 'group_by(.name)
| map({app: .[0].name, minutes: (map(.foreground) | add)})
| sort_by(-.minutes) | .[:10]'Identify the computer with one of --id, --serial, --udid, or --name, and give the range as --start+--end (yyyy-mm-dd) or --last (30d, 2w). See Jamf Pro Commands#Application Usage (Classic API, v1.25.0+) for the output columns.
The multi command runs any command against multiple profiles in one shot. Select profiles by glob pattern, explicit list, file, or at the prompt.
# Interactive profile selection (prompts with numbered list)
jamf-cli multi pro overview
# Run against all pro- profiles (glob pattern)
jamf-cli multi --filter 'pro-*' -- pro computers list -o table
# Explicit profile list
jamf-cli multi --profiles pro-school1,pro-school2 -- pro overview
# From a file (profile names or instance URLs: the same file setup takes)
jamf-cli multi --from-file instances.txt -- pro buildings apply --from-file building.json --yes
# Check version across all instances
jamf-cli multi --filter 'pro-*' -- pro jamf-pro-version list# Aggregate patch compliance across all managed instances
jamf-cli multi --filter 'pro-*' -- pro report patch-status
# Fleet-wide security posture
jamf-cli multi --filter 'pro-*' -- pro report security
# See each instance's output separately
jamf-cli multi --filter 'pro-*' --sequential -- pro report patch-status# Compare device counts across environments
for env in prod staging dev; do
count=$(jamf-cli pro computers list -p "$env" -o json --no-input 2>/dev/null | jq length)
echo "$env: $count computers"
done
# Side-by-side category comparison
diff <(jamf-cli pro categories list -p prod --field name | sort) \
<(jamf-cli pro categories list -p staging --field name | sort)Recommended flags for unattended scripts: --no-input --no-color -o json
# Exit code checking
jamf-cli pro computers list -p prod -o json --no-input > /dev/null 2>&1
rc=$?
case $rc in
0) echo "OK" ;;
3) echo "Auth failed: check credentials" ;;
4) echo "Not found" ;;
5) echo "Permission denied: the message names the permission and where to grant it" ;;
8) echo "Refused by policy: wrong credential class for this command" ;;
*) echo "Error (exit code $rc)" ;;
esacExit 8 means the command is well-formed and the resolved credentials cannot serve it: a Pro/Classic command outside the gateway's published API on a gateway profile, or a Platform-only command on an instance profile. Switch profile; a retry on the same credentials gets the same answer.
jamf-cli pro computers lock --serial "$serial" --yes --no-input
if [ $? -eq 8 ]; then
# refused by policy on this credential: retry against the instance profile
jamf-cli -p instance pro computers lock --serial "$serial" --yes --no-input
fiAn empty list prints []. A bare | jq '.[]' is safe on an instance where the collection holds nothing:
jamf-cli pro computers list --all -o json --no-input | jq -r '.[].general.name'
# A filter that matches nothing prints [] and exits 0
jamf-cli pro categories list --filter 'name=="zzz-no-such"' -o json | jq -r '.[].name'See Error Handling & Exit Codes for the full exit code table and CI/CD & Scripting for script templates, GitHub Actions examples, and Docker patterns.
Repository · Issues · Releases
jamf-cli Wiki
- Home
- Community
- Getting Started
- CLI Reference
- Product Commands
- Workflows
- Configuration
- Reference
Products
- Jamf Pro:
jamf-cli pro - Jamf Platform API:
jamf-cli pro(blueprints, benchmarks, DDM reports) - Jamf Platform:
jamf-cli platform(AI Governance, Jamf Account, audit) - Jamf Protect:
jamf-cli protect - Jamf School:
jamf-cli school - Jamf Security Cloud:
jamf-cli security