Octory 3.1.0 introduces Row & Column layout components for multi-column slide design, window-wide slide background images, Messages — banners and floating windows your scripts can send at any moment through octo-notifier — 20 live device-telemetry placeholders, and new configuration keys: installation progress in the navigation bar, animated symbols, configuration chaining, and more.
Version 3.1.0 · July 2026 · requires octo-notifier 1.1.0 for Messages.
Row & Column layout components
Until now, a container stacked its components in a single vertical flow. Two new component types unlock real composition: a Row lays out children side by side with precise width control, and a Column stacks several components inside one of those cells — or groups components anywhere under a shared condition.
A typical layout: one Row with two cells — an Image at 35% of the width beside a Column at 65% that stacks three components.
| Cell | Width | Contents |
|---|---|---|
| Row cell | 0.35 | Image |
| Column | 0.65 | Text (markdown) · validated Input · conditional Text |
Row — Type: Row
| Key | Description | |
|---|---|---|
Components | Required | The children, side by side, in order. Each entry is a normal component dictionary — with one addition below. Cannot be empty. |
WidthRatio | Optional | Written inside each child, next to its Type: the share of the row width, greater than 0 and at most 1. All or none — if one child declares it, all must, and the values must sum to 1.0. Omitted everywhere, children share the width equally. |
Margins / Border / Condition | Optional | Exactly like on any other component. The condition shows or hides the whole row. |
Allowed children: Text, Button, Image, SFSymbol, Video, Input, Column. A row cannot nest another row, and a child cannot set ShouldFillSpace.
Column — Type: Column
| Key | Description | |
|---|---|---|
Components | Required | The children, stacked top to bottom. Plain component dictionaries — no width ratios in a column. Cannot be empty. |
Margins / Border / Condition | Optional | Shared by the whole group — a convenient way to show or hide several components with one condition. |
Allowed children: Text, Button, Image, SFSymbol, Video, Input, Spacer (with an explicit Height). Groups nest at most two levels: Row → Column → components.
Copy-paste example
A ready-made slide: drop this into your configuration’s Slides array. The input is validated, so the whole row gates the Next button until at least 3 characters are typed — and typing test reveals the conditional line.
<dict>
<key>Containers</key>
<array>
<dict>
<key>Components</key>
<array>
<dict>
<key>Type</key><string>Row</string>
<key>Components</key>
<array>
<dict>
<key>Type</key><string>Image</string>
<key>URL</key><string>/Library/MyOrg/welcome.png</string>
<key>Height</key><real>96</real>
<key>WidthRatio</key><real>0.35</real>
</dict>
<dict>
<key>Type</key><string>Column</string>
<key>WidthRatio</key><real>0.65</real>
<key>Components</key>
<array>
<dict>
<key>Type</key><string>Text</string>
<key>Text</key><string>## Tell us about this Mac
Type **test** below to reveal the hidden line.</string>
</dict>
<dict>
<key>Type</key><string>Input</string>
<key>InputType</key><string>Text</string>
<key>Label</key><string>Computer purpose</string>
<key>Variable</key><string>COMPUTER_PURPOSE</string>
<key>Placeholder</key><string>e.g. development</string>
<key>Validation</key>
<dict>
<key>Regex</key><string>^.{3,}$</string>
<key>WarningText</key><string>Enter at least 3 characters</string>
</dict>
</dict>
<dict>
<key>Type</key><string>Text</string>
<key>Text</key><string>You typed *test* — this line is condition-driven.</string>
<key>Condition</key><string>COMPUTER_PURPOSE == 'test'</string>
</dict>
</array>
</dict>
</array>
</dict>
</array>
</dict>
</array>
</dict>
Good to know
- Inputs behave exactly like top-level ones. A validated input nested in a group makes the group mandatory and blocks navigation until it’s valid; typed values persist across slide navigation; warnings appear on the input itself.
- Conditions compose. When a condition hides a child, the remaining children keep their own ratios, renormalized over the full width. A group whose visible children render nothing disappears entirely — no empty band.
- Validation is your friend. A misconfigured group fails to load with a precise error naming the exact entry — for example a ratio outside ]0, 1], ratios that don’t sum to 1.0, a forbidden child type, or a heightless
Spacerin a column.
Slide background images
A slide can now paint a window-wide backdrop: the new BackgroundImage key draws a picture behind everything — the container columns, the navigation bar and the side container — and BackgroundImageAlpha fades it from full-strength photo to subtle watermark. The backdrop fills the whole window, showing through the slide padding, the gutter between containers, and around the navigation bar’s floating glass capsule.
| Key | Description | |
|---|---|---|
BackgroundImage | Optional | A local image path or URL, on any entry of Slides (and on MenuBarSlide). The image is aspect-filled: window resizes crop it, never distort it — unlike the container-level BackgroundImage, which stretches. |
BackgroundImageAlpha | Optional | The image opacity, 0…1, default 1. Low values turn the picture into a watermark behind the content. A value outside the range fails the configuration load. |
Copy-paste example
<key>Slides</key>
<array>
<dict>
<key>BackgroundImage</key>
<string>https://example.com/brand/backdrop.jpg</string>
<key>BackgroundImageAlpha</key>
<real>0.3</real>
<key>Containers</key>
<array>…</array>
</dict>
</array>
Good to know
- Backdrops crossfade. Navigating between slides fades one backdrop into the next; consecutive slides sharing the same image keep it perfectly steady — the image is not reloaded.
- Containers stay opaque. The picture shows through the slide padding and the gutters between containers. For a full-photo look, use a single container with generous margins — or a low
BackgroundImageAlphafor an all-over watermark. - Remote images load asynchronously. A URL backdrop appears a beat after the slide on first display; local paths are instant.
- Menu-bar popover. The keys are accepted on
MenuBarSlide, but the popover’s full-bleed opaque containers currently occlude the image entirely.
Messages — reach users at any moment
The octo-notifier command-line tool (v1.1.0, installed at /usr/local/bin/octo-notifier with Octory) gains a message command. Your deployment scripts can now display a message at any point — even when Octory isn’t running: the app launches just to show it.
🔔 Banner — A Notification Center banner — quiet by default, with an optional sound and an action button. Ideal for progress and completion notices.
🪟 Window — A floating, chromeless Octory window — markdown body with clickable links, an SF Symbol or image icon (optionally animated), up to three buttons. It grows to fit long messages and never truncates.
Examples
# Quiet banner into Notification Center
octo-notifier message "Enrollment complete" --body "Your Mac is ready."
# Banner with an action button running a command when clicked
octo-notifier message 'Updates pending' -b 'Restart required' \
--style banner --sound default \
--button 'Update now:open "x-apple.systempreferences:com.apple.Software-Update-Settings.extension"'
# Floating window: animated icon, dismiss + command buttons
octo-notifier message 'Welcome' -b 'Setup continues in the background.' \
--style window --icon checkmark.seal --icon-animation bounce \
--button 'OK' --button 'Details:open https://example.com/onboarding' --position top-right
# Post only if Octory is already running
octo-notifier message "Sync done" --no-launch
Options
| Option | Applies to | Behavior |
|---|---|---|
title · --body / -b | both | The window body supports basic markdown — bold, italic, code, lists, clickable links — plus \n for line breaks. |
--style | — | banner or window. |
--button | both | "Label" or "Label:command", up to 3. The command is any shell line, run as the logged-in user when clicked. Split on the first colon, so commands keep their own colons (URLs…); labels cannot contain one. Window buttons always dismiss; banner buttons require a command. |
--icon | window | An SF Symbol name, an image file path or an image URL — auto-detected. Images are sized to the symbol box. |
--icon-animation | window | bounce, pulse, wiggle, rotate, breathe, variableColor — SF Symbol icons only. |
--width · --height · --position | window | Position: center, top-left, top-right, bottom-left, bottom-right. |
--sound | banner | default plays the standard notification sound. |
--no-launch | both | Post only if an Octory instance is running; otherwise exit with an error. |
--app-path | both | Octory location when launching (default /Library/Application Support/Octory/Octory.app). Root callers — MDM scripts — are routed into the console user’s session automatically. |
Banner delivery requires notification permission. Either the user allows Octory notifications, or you deploy an MDM Notifications payload for
com.amaris.octoryto guarantee banners. Without it, messages are delivered quietly to the Notification Center list.
Testing by hand in zsh?
!triggers history expansion even inside double quotes — use single quotes for punctuation:octo-notifier message 'Save your work!'. Deployment scripts are unaffected.
⚠️ Security model. Anything running locally can post messages to Octory, and a button’s command runs as the logged-in user — but only after a deliberate click on the rendered button, never on delivery (the same trust model as swiftDialog). Don’t put secrets in message payloads. A successful exit means the message was posted, not necessarily displayed.
Device telemetry placeholders
Twenty new runtime ${…} placeholders expose live device health, security posture, MDM enrollment, update status and network state. Use them in any text interpolation or Condition — values refresh while Octory runs, and conditions re-evaluate as they change.
| Placeholder | Value | Refresh |
|---|---|---|
DEVICE_UPTIME_DAYS | days since boot, plain integer (12) | 60 s |
DEVICE_LAST_REBOOT_DATE | localized date (10 Jul 2026 at 09:14) | 60 s |
DEVICE_DISK_FREE · DEVICE_DISK_TOTAL | localized size like the Finder (245.1 GB) | 60 s |
DEVICE_DISK_FREE_GB | plain decimal GB, for conditions (245.1) | 60 s |
DEVICE_DISK_USED_PERCENT | plain integer (78) | 60 s |
DEVICE_MEMORY_TOTAL | localized size (18 GB) | 60 s |
DEVICE_MEMORY_PRESSURE | Normal · Warning · Critical | 60 s + pressure events |
DEVICE_THERMAL_STATE | Nominal · Fair · Serious · Critical | 60 s + thermal events |
SECURITY_FILEVAULT_ENABLED · SECURITY_GATEKEEPER_ENABLED · SECURITY_FIREWALL_ENABLED | true · false | 60 s |
SECURITY_SIP_ENABLED | true · false · Custom | 60 s |
MDM_ENROLLED | true · false | 60 s |
MDM_ENROLLMENT_STATUS | None · Automated · User Approved · Manual | 60 s |
OS_PENDING_UPDATES_COUNT | plain integer — as fresh as the OS’s own background scan | 300 s |
OS_LAST_UPDATE_CHECK_DATE | localized date | 300 s |
NETWORK_INTERFACE_TYPE | Wi-Fi · Wired · Cellular · Other · Offline | on change |
NETWORK_IP_ADDRESS | primary IPv4 (10.0.1.23), empty when offline | on change |
NETWORK_WIFI_SSID | SSID — empty without a Location Services grant (macOS privacy; cannot be pre-granted via MDM) | on change |
In text and in conditions
<!-- interpolated in a Text component -->
<key>Text</key>
<string>**${DEVICE_DISK_FREE}** free of ${DEVICE_DISK_TOTAL} — pressure: ${DEVICE_MEMORY_PRESSURE}</string>
<!-- gating components (or whole Column groups) -->
<key>Condition</key><string>SECURITY_FILEVAULT_ENABLED == true</string>
<key>Condition</key><string>DEVICE_DISK_FREE_GB < 20</string>
<key>Condition</key><string>MDM_ENROLLMENT_STATUS == 'Automated'</string>
Value-format rules. Boolean tokens are lowercase
true/false, so they work directly in boolean conditions (the legacy battery placeholders keep their historicalYes/Noand only match string equality). Numeric tokens are plain non-localized numbers, safe in</>comparisons — but the display sizes (DEVICE_DISK_FREE,DEVICE_DISK_TOTAL,DEVICE_MEMORY_TOTAL) are localized strings with units: compare withDEVICE_DISK_FREE_GBorDEVICE_DISK_USED_PERCENTinstead. Every placeholder starts as an empty string until its first resolution: a condition on an unpopulated token evaluates as not-satisfied, then re-evaluates when the value lands.
The DeviceHealth sample configuration shipped with Octory exercises every token — interpolations, boolean, numeric and vocabulary conditions — and makes a handy live dashboard for testing.
New configuration keys
Five additions to the configuration file — from live installation progress in the navigation bar to chaining a second configuration after the first one completes.
Navigation.ShowsInstallationProgress
Optional — Boolean, default false. The navigation bar shows a compact progress bar centered between the Previous and Next buttons, with a live status line over the monitored installations — “Working on Jamf, Slack”, “2 of 3 steps complete”, “All steps complete”. It tracks the same set of monitors as the global installation state, so you can drop the MonitorsDisplay component entirely and still let users watch their apps arrive while they browse slides.
<key>Navigation</key>
<dict>
<key>ShowsInstallationProgress</key><true/>
</dict>
AppTermination.NextConfigurationPath
Optional — A configuration name or absolute path. When Octory terminates normally, it relaunches itself with the referenced configuration — perfect for chaining a welcome flow into a second, lighter configuration. The chain is one-shot (the relaunch uses --config without --save, so the saved default configuration is untouched), and it never triggers on force-quit or system-initiated terminations (logout, restart, shutdown). Related: the -c/--config launch argument now also accepts an absolute file path.
<key>AppTermination</key>
<dict>
<key>NextConfigurationPath</key><string>/Library/MyOrg/Octory-phase2.plist</string>
</dict>
Admin.HideDockIcon
Optional — Boolean, default false. When set, Octory stays out of the Dock — the window modes otherwise promote the app to a regular Dock app at launch. Applied at launch and on the admin interface reload (⌘R).
Input.SavesSensitiveInputs
Optional — Boolean, default false. Values entered in secure (password) inputs are no longer persisted in plain text to the inputs file — set this key to true only if you need the previous behavior. Excluded variables are listed in a warning log, and variables bound to secure inputs are always censored in the variable logs, whichever action updates them.
⚠️ Behavior change. If a workflow depended on reading secure-input values back from the inputs file, it needs this key set — review before rolling out.
SFSymbol Animation
Optional — A dictionary on SFSymbol components that plays a symbol effect. Motion effects are suppressed when the user enables Reduce Motion — the non-motion Pulse and VariableColor keep running.
| Key | Description | |
|---|---|---|
Effect | Required | Bounce, Pulse, VariableColor, Wiggle, Rotate, Breathe, DrawOn, DrawOff. |
Direction | Optional | Bounce: Up/Down · Rotate: Clockwise/CounterClockwise · Wiggle: any of Up, Down, Left, Right, Clockwise, CounterClockwise, Forward, Backward. The other effects reject a direction. |
Layers | Optional | WholeSymbol, ByLayer, or Individually (DrawOn/DrawOff only). Absent: the effect’s default. VariableColor distributes over layers by design and rejects this key. |
Repeats | Optional | Integer ≥ 1 — the effect plays that many times. Absent: repeats forever. DrawOn/DrawOff play once and reject it. |
Speed | Optional | Playback speed multiplier > 0, default 1. |
Delay | Optional | Seconds between two repetitions — or before the one-shot DrawOn/DrawOff plays. Absent: repetitions chain immediately. |
Cumulative · Reversing · HideInactiveLayers | Optional | VariableColor refinements only: activated layers stay active (vs one at a time), the activation plays backwards (vs restarting), inactive layers are hidden (vs dimmed). |
<dict>
<key>Type</key><string>SFSymbol</string>
<key>Name</key><string>arrow.trianglehead.2.clockwise</string>
<key>Animation</key>
<dict>
<key>Effect</key><string>Rotate</string>
<key>Direction</key><string>Clockwise</string>
<key>Repeats</key><integer>3</integer>
<key>Speed</key><real>0.8</real>
</dict>
</dict>
As everywhere in 3.x, invalid combinations — a Direction on Breathe, Repeats on DrawOn, a VariableColor refinement on another effect — fail the configuration load with a message naming the exact key.
Also good to know when authoring for 3.x
- Markdown in
Textcomponents — headings, bold/italic, code, lists and links render styled from the component’s font configuration; plain text renders unchanged, and bare URLs stay clickable only withLinksAreEnabled. - Container breathing room — containers apply a default 15 pt inner padding, and the container-level
Marginskey is now honored (an explicit0restores a flush edge). Media components (Image,Carousel,Web,Video,PDF) bleed to the container edges. - Strict variable validation — a configuration referencing an undefined
${VARIABLE}fails to load with a precise error instead of displaying raw placeholders.
Admin quick start — test it in five minutes
- Start from a sample. Copy one of the sample configurations shipped with Octory (for instance Simple) and paste the Row/Column slide from above into its
Slidesarray. - Launch against your file.
Octory.app/Contents/MacOS/Octory --config /path/to/Test.plist— the path form leaves your saved default configuration untouched. Or install it as/Library/Application Support/Octory/Octory.plist. - Play with the slide. Resize the window (the 35/65 split holds), leave the input empty and hit Next (blocked, warning on the input), type
test(the hidden line appears). - Send yourself a message. With Octory still open:
octo-notifier message 'Hello from 3.1.0' -b 'It **works**.\nEnjoy!' --style window --icon party.popper - Flip on the extras. Give the slide a
BackgroundImage(anyhttpsURL or local picture) withBackgroundImageAlphaat0.3and watch it fill the whole window; paste${DEVICE_DISK_FREE} free of ${DEVICE_DISK_TOTAL}into aTextcomponent; setNavigation.ShowsInstallationProgresstotrueif your configuration has monitors; add anAnimationdictionary to anSFSymbol; and pointAppTermination.NextConfigurationPathat a second test file to watch the chaining.
Typos surface immediately. Configuration validation is strict: an undefined
${VARIABLE}, a bad width ratio or a forbidden child fails the load with a message naming the exact key path — check the application logs and fix from there.
Octory 3.1.0 · Messages require octo-notifier 1.1.0 (bundled with the distribution package). Full documentation and key reference: docs.octory.io.