-
Notifications
You must be signed in to change notification settings - Fork 4
CLI Patterns
Patterns shared by Jamf Pro, Jamf Protect and the Platform API. Product pages link here for the detail.
jamf-cli <product> <resource> <action> [flags]
Products: pro (Jamf Pro + Platform API), protect (Jamf Protect), school (Jamf School).
Most resources take list, get, create, update, delete and apply (name-based upsert), plus resource-specific actions.
Singleton resources (settings-style objects with one instance and no {id} in the API path) expose get and update only. Examples: cache-settings, client-check-in, re-enrollment, and the nested self-service settings. They carry no list, no apply and no --name lookup.
A sub-path with its own write methods is a command of its own, so a pro command can run three tokens deep: pro sso-settings cert get, pro csa token get, pro local-admin-password settings update. Nine qualify, each one carrying its own PUT, PATCH or DELETE in the spec. A nested command keeps the flags, scaffold, confirmation and gateway annotation its flat form had. pro sso-settings delete no longer exists: to delete the SSO certificate, run pro sso-settings cert delete.
Since v1.29.0:
proresource names come from the Jamf Pro API's own tags and URL paths. 163 resources became 135 (38 renamed, 24 merged, 13 split). 100 former names still resolve, each printing a warning that names its replacement, until 2027-03-09; three refuse. Pro Command Renames has the old→new table and the four refusal classes.
A command's Use string is enforced. A leaf that documents a placeholder takes it; a leaf that documents none refuses one, with exit 2:
$ jamf-cli pro categories list junkarg
"jamf-cli pro categories list" takes no positional arguments, but got "junkarg"
hint: run jamf-cli pro categories list --help for the flags it acceptsMove the value onto the flag that takes it: pro backup /tmp/out is refused, and the directory belongs on --output. A wrapper passing a stray token fails here where earlier releases ran the command and discarded the token.
The declared ceiling holds under --scaffold too, so <resource> update <id> extra --scaffold is refused. multi forwards every positional to the inner command it runs.
Read the Usage: line in --help for the contract; Example lines illustrate it.
The commands subcommand outputs the full command catalog in any format. No auth required.
# Machine-readable catalog for scripts and AI agents
jamf-cli commands -o json
# Human-readable table
jamf-cli commands -o table
# Include aliases and flags columns
jamf-cli commands -o table --wideOne row of the JSON output:
{
"aliases": "computer-smart-groups, computers-inventory, erase-device-computers, remove-computer-mdm-profiles, computers, comp",
"api": "pro",
"command": "pro computer-inventory list",
"description": "Return paginated Computer Inventory records",
"destructive": false,
"flags": "--all, --filter, --limit, --page, --page-size, --section, --sort",
"gatewayPermissions": ["Inventory > Devices: Read (devices:read)"],
"gatewayPrivileges": ["devices:read"],
"group": "Computer Management",
"privileges": ["Read Computers"],
"product": "pro"
}command carries the product token, and flags lists the command's own flags only; the global ones from the root are not repeated per row.
For detailed usage of any command, append --help:
jamf-cli pro computers --help # See all computer actions
jamf-cli pro computers list --help # See flags for computer list
jamf-cli protect plans --help # See all plan actionsGenerated Pro and Platform commands carry two extra catalog fields, sourced from the OpenAPI spec:
-
"destructive": true: the command mutates or erases state and requires--yes. -
"privileges": [...]: the Jamf API privileges the account needs (from the spec'sx-required-privileges). Omitted when the operation declares none. Classic, Protect and School commands carry no privilege data.
{
"command": "user-accounts delete",
"destructive": true,
"privileges": ["Delete Accounts"]
}On an HTTP 403, the CLI appends the same privilege names to the error hint, e.g. the authenticated account lacks the required API privileges; check its API role. Required privilege(s): Delete Accounts. See Error Handling & Exit Codes.
Six more fields describe how a command is served, and whether it can be:
| Field | Contents |
|---|---|
api |
Which API serves it: pro, pro-classic, platform-gateway, or radar (Jamf Security Cloud's Radar API). Under security the last two sit side by side and take different credentials
|
scopes |
The Jamf Platform API scope levels a credential must be created at to reach it: organization, environment, tenant. A 403 names a capability permission and says nothing about the level, so read it here |
gateway |
unserved when the Jamf Platform gateway's published API does not carry this Pro or Classic endpoint; a gateway profile refuses it before sending. Accompanied by gatewayBasis (unpublished / probe), gatewayDetail, and gatewaySuccessor where a replacement ships |
gatewayPrivileges |
The Jamf Account capability permissions the gateway requires for a Pro or Classic endpoint. A separate vocabulary from privileges
|
gatewayPermissions |
The same requirement in the words Jamf Account's picker prints, e.g. Inventory > Device groups: Read (device-groups:read)
|
product |
pro, protect, school, security, platform, or empty for a root command |
preview |
true when the spec marks the operation Preview (x-preview): the endpoint may change or be withdrawn without a deprecation window. 13 commands carry it in v1.30.0: platform ai-policies (all ten verbs, the synthesized apply included) and platform ai-tools get / list / schema
|
The fields are positive-only: an absent field records nothing and makes no claim either way, so an absent gateway is no promise that the endpoint is served. An absent scopes means the spec says nothing about the level, which is the case for the three organization-scoped Jamf Account APIs. preview is positive-only for the same reason: most specs declare nothing, so a false on every other row would read as "this one is GA" rather than "nothing was declared".
# Which Platform commands need an environment-scoped credential?
jamf-cli commands -o json | jq -r '.[] | select(.scopes == ["environment"]) | .command'# Which commands will a gateway profile refuse?
jamf-cli commands -o json | jq -r '.[] | select(.gateway=="unserved") | .command'
# Which Security Cloud commands does the Radar API serve?
jamf-cli commands -o json | jq -r '.[] | select(.api=="radar") | .command'
# Which commands are Preview?
jamf-cli commands -o json | jq -r '.[] | select(.preview) | .command'Key a consumer on preview, not on the Preview - prefix that the upstream summary carries into description: that prefix is prose nobody in this repo controls.
jamf-cli agent-context prints a static markdown operating guide for AI agents driving the CLI: auth, global output/agent flags, exit codes, the destructive-command gate, and the mcp serve tools. It takes no flags, needs no profile or auth, and prints markdown whatever -o says.
jamf-cli agent-contextList commands that return paginated results support these flags:
| Flag | Description |
|---|---|
--all |
Fetch every page. On by default; pass --all=false for a single page |
--limit |
Maximum total results to return (0 means unlimited) |
--page |
Page to return, zero-based. Setting it returns that page alone |
--page-size |
Results per page, for a single page only. --all ignores it
|
--sort |
Sort field and direction (e.g., name:asc) |
With --all set and --page unset, the CLI fetches the pages in sequence and merges the results into a single output. It asks for the largest page the endpoint honours, 2000 rows on most Jamf Pro lists. Earlier releases asked for 100 and sent 20 times the requests.
# Get ALL computers (auto-paginates through every page)
jamf-cli pro computers list --all -o json
# Get the first 50 results (--all=false, or --page-size is ignored)
jamf-cli pro computers list --all=false --page-size 50
# Get page 2 with 25 results per page
jamf-cli pro computers list --page 2 --page-size 25
# Get page 0 alone
jamf-cli pro computers list --page 0--page-size applies to a single page only. The Jamf Pro API answers an oversized page size by clamping it, with no error, and the pagination loop reads that clamped page as the last one, so honouring the flag here would drop records and report success. The CLI says so on stderr in both directions. --quiet suppresses these lines; --no-hints does not, because a flag you typed doing nothing is not an advisory tip:
--page-size 500 ignored: --all fetches every page at this endpoint's maximum of 2000. Pass --all=false to request a single page of 500.
--page-size 20000 exceeds this endpoint's maximum of 2000; requesting 2000.
The ceiling belongs to the endpoint: 2000 on most Jamf Pro lists, 1000 on /v1/users and Platform devices/v1, 500 on AI Governance, 100 on Platform blueprints, device-groups and any service that declares no maximum. A smaller --limit lowers the page size with it, so --limit 5 asks for 5 rows rather than 2000.
Since v1.19.0: any paginated GET accepts
--all/--limitand pages through every result instead of capping at the first 100. That covers the report and action endpoints returning the same{totalCount, results}collection:patch-report, patch-policy logs, site objects, VPP location content, and smart-group membership.
--all reports progress on stderr while it pages, in place of a per-request spinner:
-
Interactive (stderr is a TTY): an in-place counter,
Fetched 1200 / 4500(orFetched 1200when the API reports no total). -
Non-interactive (piped, redirected, or
--no-color): one NDJSON line per page on stderr:{"event":"page_fetch","fetched":1200,"total":4500}. -
--quiet: suppressed.
Only stderr progress changes; stdout output is the same either way.
The --field global flag extracts a single named field from each object in the JSON response, printing one value per line. It replaces common jq one-liners for simple field access.
# List all building names (one per line)
jamf-cli pro buildings list --field name
# Extract IDs for piping
jamf-cli pro categories list --field id | while read id; do
jamf-cli pro categories get "$id" -o yaml
done
# Works with Classic API too
jamf-cli pro classic-policies list --field nameOne verb, in all four product namespaces:
jamf-cli pro open # the Jamf Pro dashboard
jamf-cli protect open
jamf-cli school open
jamf-cli security openThe URL is printed rather than launched under any of --print, --no-input, --dry-run, --out-file, --field, --select, or a non-terminal stdout. A request for data is never answered with a side effect, so | pbcopy and a CI job both get the URL where an interactive terminal gets a browser. Consume it with -o json or --field url:
jamf-cli pro open computers --field url
# https://your-instance.jamfcloud.com/computers.htmlIn the launch case stdout stays empty and the note goes to stderr, so nothing downstream reads a launch as data. $BROWSER wins where it is set, ahead of the platform's own opener.
pro open differs twice over. It is the only one that takes a section positional (pro open [<section>], plus --list to print every name), and the only one that can send a request: a platform gateway profile has no Jamf Pro host to use, so the URL is read from the Jamf Pro API. protect open, school open and security open send nothing and need no credentials. Per-product URL resolution, the section table and the scope a gateway profile needs are on Jamf Pro Commands, Jamf Protect Commands, Jamf School Commands and Jamf Security Cloud Commands.
Commands that accept a request body (create, update, apply) support --scaffold, which prints a JSON template of the writable fields with type-appropriate defaults and example values where the spec supplies them.
# Show the JSON template for creating a building
jamf-cli pro buildings create --scaffold
# apply also supports --scaffold
jamf-cli pro buildings apply --scaffold
# Pipe scaffold into an editor, fill in values, then create/apply
jamf-cli pro buildings create --scaffold > building.json
# ... edit building.json ...
cat building.json | jamf-cli pro buildings apply --yes
# Output the scaffold as YAML instead of JSON
jamf-cli pro buildings create --scaffold -o yaml
# Protect commands also support --scaffold
jamf-cli protect plans apply --scaffold
jamf-cli protect analytics apply --scaffoldThe scaffold drops read-only fields (like id), nested ones as well as top-level, and keeps write-only ones, since a request body needs them. Fields with example values in the API spec use those examples; the rest use type-appropriate defaults ("" for strings, 0 for numbers, false for booleans, {} for objects). An array whose element is an object shows one specimen element, so you see the shape in place of an uninformative []; an array of plain scalars stays empty.
All four generators (modern Pro, Classic, Platform, Security Cloud) render scaffolds through one shared walker, so the rules hold everywhere.
A patch or an action under a path parameter with no name lookup accepts --scaffold with no positionals, across 26 leaves:
jamf-cli pro enrollment-customization ldap-update --scaffold
jamf-cli pro mdm-renewal-device-common-details patch --scaffoldOnly the floor moves. The declared ceiling still applies, so ldap-update 1 2 3 --scaffold is refused. Classic create / update on an id-only resource take the same relaxation, and a Classic scaffold bypasses the output formatter, so the XML template prints at its own indentation.
v1.30.0 hit the same trap through --all. Cobra validates Args before RunE, so the emitted ExactArgs refused pro jamf-pro-notifications delete --all before the --all branch could be read, and the flag was unreachable. It went unnoticed because every previously paired command also had a --name lookup, whose MaximumNArgs(1) relaxed the floor by accident; notifications is the first with neither a name lookup nor a scaffold. The template now relaxes the floor while --all is set and keeps the declared ceiling, exactly as --scaffold does.
Classic create, update and apply also take --scaffold, printing an XML template. 114 of the 117 Classic write commands carry it. The shape comes from a committed schema artifact derived from the published Classic API documentation, which also supplies the required/optional and enum lists in --help.
jamf-cli pro classic-policies create --scaffold
jamf-cli pro classic-printers update --scaffoldThe three without it are classic-computer-configs create, update and apply: the resource is dead, and the instance 404s it too.
Most scaffolds need values filled in. A few render a specimen value the server rejects outright, and a Classic policy is the sharpest case:
-
general.category.id's spec example is0, which answers409 No match found for category 0. - The
scopeandaccount_maintenancespecimen references point at objects that do not exist on your instance, and answer500.
A scaffold shows one specimen per optional section, so delete the sections you do not need. The generated help says so.
--help lists the fixed value set of a constrained request field under "Allowed values:", one line per dot-path field:
Allowed values:
ipsec.keyExchange: ikev1, ikev2
selectedOsVersions[].osType: MAC_OS, IOS, VISION_OS
A [] suffix on the path means each array element carries the constraint. --scaffold renders an enum as "", so read both: the scaffold for the shape, the help for the legal values.
Resources that support both create and update have an apply command that performs a name-based upsert. It reads input, extracts the name (or displayName), checks whether a matching resource exists, then creates or replaces it. Singleton resources have no apply: one instance exists, so upsert by name has nothing to match.
apply is also generated for resources exposing create + patch with no full update / PUT. The replace branch then issues PATCH with application/merge-patch+json, so an idempotent upsert still works and field omissions leave those fields unchanged. Added in v1.12.0 for volume-purchasing-locations, computer-inventory, mobile-device-groups-static-groups and patch-software-title-configurations (the first two were vpp-locations and computers-inventory before v1.29.0 renamed them).
Since v1.27.0:
applyand--nameare generated only where the resource's collection endpoint answers a GET, since that lookup is how both find an existing record. Resources whose modern API is POST-collection + GET-{id}only have noapply:adcs-settings,cloud-azure,cloud-ldap,digicert,dock-items,team-viewer-remote-administration,venafi. Those pluscertificate-authority,classic-ldap,computer-groups,computer-inventory-collection-settings,conditional-access,icon,managed-software-updates,mdm-renewalandjamf-pro-user-account-settingscarry no--name. (Names as of v1.29.0; seven of those resources had different names when the change shipped, see Pro Command Renames.) Target these by<id>and usecreate/update/patch.
Modern API apply (and create/update) commands accept both JSON and YAML input; the CLI tries JSON first, then falls back to YAML parsing.
# Create or update a building by name, from stdin (JSON or YAML)
echo '{"name":"HQ","streetAddress1":"1 Apple Park Way"}' | jamf-cli pro buildings apply
echo 'name: HQ\nstreetAddress1: "1 Apple Park Way"' | jamf-cli pro buildings apply
# From a file (JSON or YAML)
jamf-cli pro buildings apply --from-file building.json
jamf-cli pro buildings apply --from-file building.yaml
# Skip replacement confirmation
jamf-cli pro buildings apply --from-file building.json --yes
# Preview what would happen
jamf-cli pro buildings apply --from-file building.json --dry-run
# → [dry-run] Would create building "HQ"
# → [dry-run] Would replace building "HQ" (id: 42)
# Print a JSON scaffold template (also works on apply)
jamf-cli pro buildings apply --scaffoldClassic API resources accept XML input. The CLI extracts the name from the XML body, from either a top-level <name> or a nested <general><name>.
# Apply a policy from XML
cat policy.xml | jamf-cli pro classic-policies apply --yes
# Apply a printer from a file
jamf-cli pro classic-printers apply --from-file printer.xml| Flag | Description |
|---|---|
--from-file |
Path to input file (JSON/YAML for modern API, XML for classic API). Omit it and the body is read from stdin. Since v1.29.0 it is the one name across all four products; Platform and Security Cloud took --file before that, with no compatibility alias left behind. --file now names an upload payload on the commands that send one (pro packages upload, protect analytics import), which cannot be piped |
--scaffold |
Print a JSON template of the input format (no auth required, skips apply logic) |
--yes |
Skip replacement confirmation (still prompts for collision resolution) |
-n, --dry-run
|
Resolve existence and preview, but don't create or replace |
apply detects a name that several resources share:
- Interactive mode: prompts you to pick which resource (by ID) to replace
-
--no-inputmode: fails with an error listing all matching IDs
--yes skips the replacement confirmation and leaves collision resolution alone. A collision requires an explicit choice.
Six gateway resources gained apply in v1.29.0: security dns-zones, security ztna-apps, security ztna-gateways, security ztna-grouped-gateways, security device-groups and platform ai-policies.
These six update with PATCH, so fields you omit from the body keep their current values, where a Pro or Classic apply replaces the record. Each command's --help states which it sends:
The update is a PATCH: fields you omit keep their current values. To clear a
field, send it explicitly.
platform ai-policies is the exception within the exception, and its help says so: the server replaces settings wholesale despite the merge-patch content type, so send that field complete or lose the parts you left out. Its other top-level fields merge.
Three further properties to rely on:
-
The name comes from the body. No
--nameflag sits beside it. A body with noname, or a non-string or empty one, is refused before the CLI sends a request. - Only a "not found" on the exists check takes the create branch. An auth error or a 5xx during the lookup aborts the command instead of creating a duplicate.
-
The exists check and the create are separate requests, so two runs racing on the same absent name (a CI retry, or concurrent jobs) can both create one. No spec here declares a name-uniqueness
409, so serialiseapplyper resource where that matters.
platform ai-policies apply writes a draft, which the server enforces once you publish it, so follow a successful apply with platform ai-policies publish <id>. Publishing is not idempotent: with nothing pending it answers 409.
The Jamf Security Cloud Radar commands carry no apply. That surface holds singletons whose update is already an idempotent create-or-replace (stream, status), actions (risk override, verification trigger, device-lifecycle purge) and read-only documents (well-known, jwks), so there is no named collection to match a name against.
# Export, modify, and re-apply
jamf-cli pro buildings get --name "HQ" -o json | jq '.city = "Cupertino"' | jamf-cli pro buildings apply --yes
# Clone a resource with a new name
jamf-cli pro departments get --name "Engineering" -o json | jq '.name = "Engineering - EMEA"' | jamf-cli pro departments applyEvery resource that supports export produces output you can pipe back into apply. Exported files carry names for cross-resource references in place of IDs, which makes them portable across tenants.
# Round-trip: export from one tenant, import to another
jamf-cli protect plans export "Default Plan" \
| jamf-cli protect plans apply --yes -p protect-staging
# Export to YAML, edit, re-import
jamf-cli protect analytics export "Suspicious Login" > rule.yaml
# ... edit rule.yaml ...
jamf-cli protect analytics apply --from-file rule.yaml --yes
# Blueprint round-trip (Platform commands take a positional <id>, so look up by --name)
jamf-cli pro blueprints export --name "macOS Standard" -o yaml > bp.yaml
jamf-cli pro blueprints apply --from-file bp.yaml --yes -p staging-platformThe blueprint YAML round trip needs a v1.31.1 export. Earlier releases wrote each component's
configurationin a shapeapplycannot read back.applyrefuses such a file and tells you to re-export it.-o jsonwas correct throughout, and this never touched the Protect or School exports.
The --name flag sits on the get, update and delete commands themselves, for resources that support name-based lookup. There are no separate get-by-name or delete-by-name subcommands.
# Get by name
jamf-cli pro buildings get --name "HQ"
jamf-cli pro computers get --name "Neil's MacBook"
# Update by name (targeted field changes with --set)
jamf-cli pro buildings update --name "HQ" --set name="HQ West"
# For a full-body update from a file, use apply (upsert):
jamf-cli pro buildings apply --from-file building.json
# Delete by name
jamf-cli pro buildings delete --name "HQ" --yes
jamf-cli pro buildings delete --name "HQ" --dry-run
# → [dry-run] Would delete building "HQ" (id: 42)Per-resource lookup fields are also available where they make sense:
| Flag | Resources |
|---|---|
--name |
Resources whose collection endpoint answers a GET (see the v1.27.0 note under Apply) |
--serial |
computers, mobile-devices (on get and patch) |
--udid |
computers, mobile-devices (on get and patch) |
--serial/--udid resolve a target on get and patch only; delete looks up by --name (or --group).
# Get a computer by serial number
jamf-cli pro computers get --serial C02X1234
# Get a mobile device by UDID
jamf-cli pro mobile-devices get --udid "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
# Delete a mobile device by name
jamf-cli pro mobile-devices delete --name "Lab iPad 12" --yesCollision detection works as it does for apply: a --name, --serial or --udid value matching more than one record prompts you to pick one. Under --no-input (or a non-terminal stdin) the lookup fails with a usage exit code (2) in place of resolving to an arbitrary match.
Since v1.23.0: the CLI surfaces duplicate computer serial numbers (e.g. after a logic-board swap). On an ambiguous serial-based lookup, re-run against a specific
<id>, or runjamf-cli pro report duplicate-serialsto list the colliding records.
Classic API resources also support --name on get and delete, resolving via URL-based name paths (e.g., /JSSResource/policies/name/<value>).
Protect and School commands take a positional <name> argument in place of --name:
jamf-cli protect plans get "Default Plan"
jamf-cli protect analytics delete "Old Rule" --yes
jamf-cli school devices get "MacBook-Pro"
jamf-cli school classes get "Math 101"--name and apply both list the collection and match. The Platform API and gateway-served Jamf Security Cloud commands share one resolver, which reports two outcomes of its own. The Jamf Pro lookup has equivalents (multiple resources found matching "…" (IDs: …), and a matched record with no usable ID reported in place of being dropped from the candidate set).
Matches accumulate across every page before the decision, so the ambiguity error fires wherever the copies sit:
ambiguous match: 2 items named "EU Gateway"; identify it by ID instead
A name matching an item the list returns no ID for gets its own error, separate from "not found". Jamf Security Cloud's device groups are the live case: the implicit "Default Group" comes back with a name and no id. Verified on an EU gateway tenant:
$ echo '{"name":"Default Group"}' | jamf-cli security device-groups apply --yes
found 1 item(s) named "Default Group" in /securitycloud/v2/groups, but the list returns no ID for
them; identify the item by ID insteadStored groups carry an id and resolve as normal.
Platform API commands take a third shape.
blueprints,compliance-benchmarks,platform-device-groups, and theschool blueprintsequivalents take a positional<id>plus a--nameflag; the Protect shape does not carry over. A name in the positional slot is sent to the API as an ID and 404s, and passing both<id>and--nameis rejected. Three shapes differ:blueprints clone <source-name> <new-name>andcompliance-benchmarks export <title>/clone <source-title> <new-title>take names as positionals;platform-devices getaccepts either a UUID or a serial number as its positional (auto-detected);ddm-reports devicetakes a device ID only, with no--namelookup.
--set key=value exists across all four command families and does something different in each. Get this wrong and the request either fails or succeeds having thrown away what you did not name.
| Family | What --set does |
Interaction with a file body |
|---|---|---|
Modern Pro update
|
Fetches the record, merges your changes, drops read-only fields, writes the whole record back (fetch-merge-replace) | Alternative to piping a full body on stdin |
Modern Pro patch
|
Builds a JSON Merge Patch body (RFC 7386); sends only what you name | Alternative to --from-file
|
Classic (pro classic-*) |
Builds the whole XML body from scratch | Mutually exclusive with --from-file |
| Platform and Security Cloud | Overlays onto the --from-file body |
Composes with --from-file; --from-file accepts JSON or YAML |
A Classic PUT is a partial update: a body of only <name> renames a network segment and leaves its address range, override flags and a group's whole criteria array intact. So --set alone is a valid update with no fetch-merge cycle to run first.
# Rename a network segment, leaving every other field alone
jamf-cli pro classic-network-segments update 4 --set name="Guest WiFi"
# Build a whole object from flags
jamf-cli pro classic-ibeacons create --set name="Room 123 Beacon" --set major=1 --set minor=2Classic --set is the one --set that validates, and it refuses four things:
- an unknown field
- an out-of-enum value
- an empty value for an enum field (
--set general.frequency="$FREQ"withFREQunset) - any credential field: a distribution point, SMTP server, LDAP server, directory binding, VPP account, JWT configuration or disk-encryption configuration password, token or key. Those belong in
--from-file, which is why the two are mutually exclusive here. Credential fields are also kept out of shell completion.
Wire-checked:
| Body | Response | Stored |
|---|---|---|
| An unrecognised element | 201 |
The server drops the element |
| An out-of-enum value | 201 |
The default (frequency: "Twice per fortnight" reads back as Once per computer) |
| An empty enum element | 200 |
The default |
A guess therefore yields a working object that does the wrong thing, with nothing in the response naming it. Read the enum list in --help.
A non-enum field still accepts key= and still renders an empty element, so clearing a field remains a valid edit.
Here --set layers on top of a body, so the pattern is scaffold → edit → --from-file, with --set for the last-minute differences. Both accept JSON or YAML in --from-file.
jamf-cli security ztna-gateways create --from-file gateway.yaml --set datacenter=eu-west-1
jamf-cli security ztna-gateways patch <id> --set ipsec.esp.lifetimeInSec=14400The body can also arrive on stdin with --from-file left off, which makes scaffold → edit → apply a pipeline:
jamf-cli security ztna-gateways apply --scaffold | vipe | jamf-cli security ztna-gateways apply --yesAn empty pipe counts as no body, since a CI runner hands every process a stdin that is not a terminal and carries nothing. A named --from-file that is empty is an error, because a file you named and a file you did not are different instructions.
Two limits. --set builds an object, so a request body that is a top-level array (the two DNS whole-list replaces) needs --from-file. And it keeps secrets off the command line: --set deviceSyncAuth.clientSecret=… would land in shell history and ps, so pass credentials in --from-file.
update --set key=value changes individual fields on resources whose API exposes a full-record PUT only (no PATCH). The CLI fetches the current record, merges your --set changes in, drops read-only fields, and writes the whole record back; omitted fields keep their current values. So single-field edits work even on resources without patch.
# Change one field on a PUT-only resource (fetch-merge-replace)
jamf-cli pro buildings update 1 --set name="HQ West"
# Multiple fields at once
jamf-cli pro buildings update --name "HQ" --set city="London" --set zipPostalCode="EC1A 1BB"
# Without --set, pipe a full JSON document to stdin to replace the whole record
echo '{"name":"HQ West"}' | jamf-cli pro buildings update 1Tab-completion for --set lists the mergeable field paths for each resource.
Since v1.23.0:
update --setperforms the fetch-merge-put for PUT endpoints. For resources that expose a PATCH endpoint, preferpatch(below), which sends the changed fields alone.
The CLI parses --set values against the field's declared type in the API schema. Array and object fields take a JSON value, and the CLI verifies the kind before sending:
# Array field: pass real JSON, quoted so the shell leaves it alone
jamf-cli pro api-roles update --name "Read Only" --set privileges='["Read Computers","Read Policies"]'
jamf-cli pro computer-prestages update 3 --set customPackageIds='["295","301"]'
# Object field
jamf-cli pro computer-prestages update 3 --set locationInformation='{"realname":"Lab","room":"2.04"}'
# Nested scalars still use dot-notation
jamf-cli pro computer-prestages update 3 --set accountSettings.adminUsername=labadminPer type:
- array / object: JSON-decoded and kind-checked; a mismatch is rejected with a hint before anything is sent
- integer / number / boolean: coerced, and non-parseable input is rejected
-
string: kept as literal text, so numeric-looking string IDs (
enrollmentSiteId=-1) go out as strings -
null: clears the field - dot-notation into an array or scalar parent: rejected in place of building a bogus nested object
The --help for update and patch lists the settable fields with their types, and calls out the array/object ones under "Array and object fields accept a JSON value". This applies to both update --set (fetch-merge-PUT) and patch --set (merge-patch).
update --set is a fetch-merge-replace, and fields the server never returns from a GET (passwords and secrets) cannot be merged back in, so a plain --set on such a resource blanks them. The CLI warns for each write-only field on the endpoint, nested ones included, and gives you the incantation that preserves it:
warning: computer-prestage field "accountSettings.adminPassword" is write-only: the server never returns it,
so this update will blank any existing value. Pass --set accountSettings.adminPassword=<value> to preserve it.
Coverage includes prestage adminPassword / recoveryLockPassword, distribution point passwords, and SMTP, GSX, SSO, LDAP and local user-account secrets. Prefer patch where the resource offers it: a merge-patch leaves fields you did not send alone.
Resources whose API exposes a PATCH endpoint get a patch subcommand with JSON Merge Patch semantics (RFC 7386). Resources with no PATCH endpoint (e.g. buildings, scripts) get no patch; use update or apply there.
# Update a single field by ID
jamf-cli pro computers patch 101 --set general.assetTag=CORP-101
# Update multiple fields at once
jamf-cli pro computers patch 101 --set general.managed=true --set general.assetTag=CORP-101
# Patch from a JSON file (pure merge-patch body)
jamf-cli pro computers patch 101 --from-file changes.json
# Preview patchable fields for a resource
jamf-cli pro computers patch --scaffold
# Look up by name instead of ID (no ID argument needed)
jamf-cli pro computers patch --name "Neil's MacBook" --set general.assetTag=CORP-101
# Per-resource lookup fields: computers and mobile devices also accept --serial and --udid
jamf-cli pro computers patch --serial C02X1234 --set general.managed=false
jamf-cli pro mobile-devices patch --serial F4K3SER1AL --set general.assetTag=ASSET-001Name the fields you want to change and omit the rest: the API changes the keys you send and leaves the others alone. Send null for a key to clear that field.
# Changes the asset tag; all other fields remain
echo '{"general":{"assetTag":"CORP-9"}}' | jamf-cli pro computers patch 101
# Clear the asset tag field
jamf-cli pro computers patch 101 --set general.assetTag=null| Flag | Description |
|---|---|
--set key=value |
Set a field using dot-notation (repeatable). Scalars are coerced to the schema's type; array and object fields take a JSON value (v1.25.1+) |
--from-file |
Path to a JSON merge-patch file |
--scaffold |
Print a template showing all patchable fields |
--name |
Look up resource by name (instead of positional ID) |
--serial |
Look up by serial number (computers and mobile devices) |
--udid |
Look up by UDID (computers and mobile devices) |
Shell completion for --set is built in: tab-complete to see the available dot-notation field paths for each resource.
For resources that carry a file payload in a single body field (scripts, config profiles, VPP tokens, DEP tokens, AppConfig), the CLI exposes dedicated file flags on create, update, apply and the relevant upload operations. You supply the resource metadata (name, category, scope) in the JSON or XML body; the file flag populates one field, and the CLI handles encoding, companion fields and name fallback.
| Flag | Resources |
|---|---|
--script-file |
scripts, computer-extension-attributes (SCRIPT inputType) |
--token-file |
volume-purchasing-locations, device-enrollments
|
--mobileconfig-file |
classic-macos-config-profiles, classic-mobile-config-profiles
|
--appconfig-file |
classic-mac-apps, classic-mobile-apps
|
# Metadata + payload together, both flag-driven
echo '{"name":"Deploy Agent","priority":"BEFORE"}' \
| jamf-cli pro scripts apply --script-file /path/to/deploy.sh --yes
# AppConfig update leaves scope, category, and all other app fields untouched
jamf-cli pro classic-mac-apps apply \
--name "Slack" \
--appconfig-file /path/to/slack-appconfig.plist \
--yes
# DEP token: apply handles both the initial upload and token rotation
jamf-cli pro device-enrollments apply \
--name "ACME Production" \
--token-file /path/to/server_token.p7m --yesChanged in v1.12.0:
device-enrollments(device-enrollment-instancesbefore v1.29.0) has the standardapplysubcommand (pluscreate/update), which composes the token-upload endpoints for you. Useapplyto create or rotate DEP tokens. Theupload-token/upload-token-by-idsubcommands and the--renameflag are gone.
See Jamf Pro Commands#File Uploads for the full reference, including the separate upload subcommands used by packages, icons, inventory-preloads and other resources with a dedicated upload endpoint.
Use --dry-run / -n to preview what a mutating command would do. Read operations (list, get) pass through; the CLI intercepts writes (create, update, delete).
$ echo '{"name":"Test Category","priority":1}' | jamf-cli pro categories create --dry-run
[dry-run] POST /v1/categories
[dry-run] Request body:
{"name":"Test Category","priority":1}
{}-
[dry-run]lines go to stderr -
{}is the synthetic response on stdout (an empty JSON object) - Use it to verify request payloads before executing
A destructive generated command declares its own --dry-run / -n, which shadows the root persistent flag, so dryRunClient never sees it and the template's own branch is the only thing honouring it. That branch sat after the --all block, which had already sent the request. pro jamf-pro-notifications delete --all --yes -n sent a live tenant-wide DELETE and got a 204; wire-checked before and after the fix. The --all block now previews ahead of both the confirmation and the request, matching the Platform and Security Cloud templates.
Non-destructive bulk --all was never affected: pro app-installers-deployments installation-retry --all -n declares no local --dry-run, so dryRunClient already covered it.
Each generated Platform and Security Cloud write reports its own preview (method, resolved path and body, on stderr) and returns without sending anything:
$ jamf-cli -p my-platform pro blueprints delete <id> -n --yes
[dry-run] DELETE /blueprints/v1/blueprints/<id>Name→ID resolution runs ahead of the preview, so -n --name "macOS Standard" reports the resolved path in place of the name you typed. A validation that would make the request unbuildable runs first too. The preview also prints ahead of any confirmation prompt, so --no-input -n <destructive command> previews the request without needing --yes.
blueprints apply / clone / deploy / import-profile and friends orchestrate several SDK calls and carry no per-command preview. Under -n the CLI refuses a mutating request from one of these: nothing is sent, the command exits 1, and the message names the blocked request.
[DRY_RUN] refused POST /blueprints/v1/blueprints: --dry-run is set and this command has no preview mode. Re-run without -n to apply.
The refusal arrives as a synthetic 412 Precondition Failed. Authenticating is exempt, so the token exchange still runs.
Commands that modify or remove resources require confirmation by default. The --yes flag skips the confirmation prompt.
| Flag | Description |
|---|---|
--yes |
Skip confirmation prompts for destructive operations |
Since v1.22.0, destructive commands are also marked "destructive": true in the jamf-cli commands -o json catalog and in the MCP list_commands tool (see MCP Server), so a client can tell in advance that --yes will be required.
All generated delete commands support bulk mode via two mutually exclusive flags:
| Flag | Description |
|---|---|
--from-file |
Path to a file listing IDs, names, or alternate identifiers to delete (one per line; blank lines and # comments ignored) |
--group |
Delete all members of a computer or mobile device group (available on computer-inventory and mobile-devices) |
Both flags take the standard lookup identifiers: numeric IDs, names, serial numbers, UDIDs and MAC addresses. The CLI resolves non-numeric entries through the resource's name-lookup path and reports the ones it cannot find as errors.
--all is a third, distinct shape: it is generated only where the API itself declares a collection-level DELETE, sends that one request in place of a loop, and refuses to be combined with a positional. pro jamf-pro-notifications delete --all is the case in v1.30.0.
--dry-run previews what would be deleted. In --no-input mode, --yes is required or the command fails.
# Delete computers from a file of serial numbers (one per line)
jamf-cli pro computer-inventory delete --from-file decommissioned.txt --yes
# Delete scripts by name from a file
jamf-cli pro scripts delete --from-file old-scripts.txt --yes
# Preview without deleting
jamf-cli pro buildings delete --from-file buildings.txt --dry-run
# Delete all members of a computer group (computer-inventory only)
jamf-cli pro computer-inventory delete --group "Decommissioned Macs" --yes
# Delete all members of a mobile device group
jamf-cli pro mobile-devices delete --group "Retired iPads" --yesThe file accepts IDs, names and alternate identifiers:
# Computers to decommission
C02X1234
C02Y5678
42
Neil's MacBook
--from-file and --group are mutually exclusive, and both are mutually exclusive with --name, --serial and --udid.
The CLI enforces a configurable delay between consecutive destructive operations on the same profile, which gives you a window to interrupt a script deleting resources in a tight loop.
- Default: 10 seconds between destructive operations
-
Configurable: Set
destructive-cooldownper profile in config (see Configuration & Profiles#Destructive Cooldown) -
CI/CD safe: Skipped when
--no-inputis set or when using environment variable auth
While the cooldown is active, the CLI prints: Cooldown: waiting 3.456s before destructive operation...
These actions count as destructive: delete, delete-multiple, erase, lock, wipe, remove, restart, shutdown.
With --no-input set (e.g. in CI/CD), destructive operations fail with an error unless --yes is also passed, which guards against data loss in automated pipelines.
# Interactive: prompts for confirmation
jamf-cli pro scripts delete 42
# Non-interactive: skips confirmation
jamf-cli pro scripts delete 42 --yes
# CI/CD: --no-input without --yes = error
jamf-cli pro scripts delete 42 --no-input # fails
# CI/CD: --no-input with --yes = runs
jamf-cli pro scripts delete 42 --no-input --yes # worksCobra skips an inherited persistent flag whose name the command already declares, and the shorthand goes with it. So on a command with its own --output, the global -o / --output output-format flag is absent.
Two commands are affected, both taking --output as a destination directory:
jamf-cli pro backup --output ./backup
jamf-cli protect backup --output ./backupOn these, -o json fails with unknown shorthand flag: 'o' in -o and exits 2. That bites hardest in CI, because JAMF_CLI_ARGS='-o json' is a documented way to set a default and the CLI prepends it to every invocation (see CI/CD & Scripting#JAMF_CLI_ARGS). Use --format (these commands' own JSON/YAML switch for the files they write) and drop -o from JAMF_CLI_ARGS for those steps.
A global flag the command inherits is honoured, and for one honoured in part the command's Long says what it leaves out: protect backup -n still writes files (that is what --output asked for) and reports the stale documents it would have pruned as [dry-run].
Each product has short aliases for its common commands. See the product-specific pages for the full alias tables:
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