OMC Skill

SkillDev tools

Create, edit, debug, rebuild, or codesign OMC applets for macOS. Use when the user asks to build/rebuild or codesign an applet, add a command, write an action script, configure Command.plist or Command.json, use omc_dialog_control, chain commands, or work with AppletBuilder (incl. the appletbuilder CLI / Build & Run). For ActionUI JSON UI design, also activate the actionui skill.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the OMC Skill skill

What this skill tells your AI

The instructions your AI receives, as published by abra-code/omc in Skill/SKILL.md and read by ahel’s review.

What is OMC

OMC (OnMyCommand) is a macOS low-code app builder engine. OMC applets are GUI .app bundles, with optional macOS Services menu items. Each applet combines a declarative command manifest (Command.plist) with shell scripts, Python scripts, or AppleScript files that implement the actions.

The OMC engine (Abracode.framework) handles app runtime, input routing, UI presentation, and script execution. A developer writes only the application-specific logic.

User should start a new applet using AppletBuilder (Distribution/AppletBuilder.app) — it creates the correct bundle structure, installs the framework, and generates the initial Command.plist and starter scripts.

OMC App Bundle Structure

MyApp.app/
├── Contents/
│   ├── Frameworks/
│   │   └── Abracode.framework       ← OMC engine (copied from AppletBuilder template)
│   ├── MacOS/
│   │   └── MyApp                    ← executable (renamed copy of OMC binary)
│   ├── Resources/
│   │   ├── Base.lproj/
│   │   │   ├── MainMenu.nib         ← app menu (from template; do not remove)
│   │   │   └── MyDialog.json        ← optional ActionUI JSON dialog
│   │   ├── Scripts/
│   │   │   ├── lib.myapp.sh         ← shared library: tool paths + control IDs
│   │   │   └── MyApp.*.sh / *.py    ← action handler scripts
│   │   └── Command.plist            ← command definitions
│   └── Info.plist

Command.plist

The command manifest lives at Contents/Resources/Command.plist (XML/binary plist) or Contents/Resources/Command.json (JSON). OMC reads either, preferring Command.json when both are present; AppletBuilder creates new applets with Command.json. The two formats are structurally identical — the same keys, value types, and VERSION == 2 rule apply (a JSON number 2 for VERSION, JSON true/false for booleans). This guide shows plist XML, but every example maps directly to JSON.

Root structure (plist):

<?xml version="1.0" encoding="UTF-8"?>
<plist version="1.0">
<dict>
    <key>COMMAND_LIST</key>
    <array>
        <!-- one <dict> per command -->
    </array>
    <key>VERSION</key>
    <integer>2</integer>
</dict>
</plist>

Root structure (JSON equivalent):

{
  "COMMAND_LIST": [
    {  }
  ],
  "VERSION": 2
}

Command Identity Keys

KeyTypeNotes
NAMEstringHuman-readable label. Required. Shared across a command group.
COMMAND_IDstringDot-notation ID (e.g. MyApp.results.selected). Required for subcommands. On the main command either omit it or set it explicitly to <NAME>.main (or bare main) — both are equivalent. The main command is also addressable for chaining by <NAME>.main, main, or the legacy internal top!. Its script filename follows the convention <NAME>.main.<ext>.
EXECUTION_MODEstringHow the command runs. Default: exe_script_file or exe_shell_script if COMMAND is set.
ACTIVATION_MODEstringWhat context information the command expects. Default: act_always.

Minimal command

<dict>
    <key>NAME</key>
    <string>MyApp</string>
    <key>EXECUTION_MODE</key>
    <string>exe_script_file</string>
    <key>ACTIVATION_MODE</key>
    <string>act_file_or_folder</string>
</dict>

Script Naming Convention

Script files live in Contents/Resources/Scripts/. The filename maps directly to a COMMAND_ID:

Script fileHandles COMMAND_ID
MyApp.main.shthe main command (no COMMAND_ID, or COMMAND_ID = MyApp.main) — MyApp is the NAME
MyApp.results.selected.pyMyApp.results.selected
MyApp.settings.save.shMyApp.settings.save
lib.myapp.sh(shared library — sourced by other scripts, not a command handler)

OMC resolves the interpreter from the extension: .sh / .bash / .zsh → corresponding shell, .py → Python, .applescript → AppleScript, .js → JSC.

No shebang line is needed. OMC supplies the interpreter path automatically.

For Python applets, the embedded Python at Contents/Library/Python/bin/python3 is used when present — no system Python dependency. When a bundle has embedded Python, OMC exports — for every handler, shell or Python — PATH (prepended with the interpreter's bin/), PYTHONPYCACHEPREFIX (the per-user /var/folders/.../T/Pyc, not $TMPDIR), and PYTHONPATH (prepended with Contents/Library/Packages/ when it exists). Install third-party modules into Contents/Library/Packages/ (e.g. python3 -m pip install --target .../Contents/Library/Packages pkg) rather than the runtime's own site-packages: the Packages/ dir survives a Python runtime upgrade/rebuild, whereas Contents/Library/Python/ is replaced wholesale. See docs/omc_python_scripting_guide.md.

Environment Variables

OMC exports these variables into every script's environment:

Always Available

VariableDescription
$OMC_APP_BUNDLE_PATHFull path to the running applet's .app bundle
$OMC_APP_PROCESS_IDPID of the host process running OMC, which for an applet is the applet itself - use it to tell whether the instance that owns some state is still alive (not the frontmost app, that is $OMC_FRONT_PROCESS_ID)
$OMC_OMC_SUPPORT_PATHPath to OMC's support directory — all runtime tools live here
$OMC_CURRENT_COMMAND_GUIDGUID of the current command invocation (pass to omc_next_command)
$OMC_OBJ_PATHPath of the file/folder that triggered the command (drag & drop / open panel / service)

Window Context

VariableDescription
$OMC_ACTIONUI_WINDOW_UUIDWindow UUID for ActionUI dialog scripts
$OMC_NIB_DLG_GUIDWindow UUID for NIB dialog scripts

Control Values

VariableDescription
$OMC_ACTIONUI_VIEW_<N>_VALUECurrent value of ActionUI element with id N
$OMC_NIB_DIALOG_CONTROL_<N>_VALUEValue of NIB control with tag N
$OMC_NIB_TABLE_<N>_COLUMN_<M>_VALUESelected row value from NIB table tag N, column M (1-based; column 0 = all columns tab-joined)

ActionUI Trigger Context

Set when a script runs as the handler for an actionID or valueChangeActionID:

VariableDescription
$OMC_ACTIONUI_TRIGGER_VIEW_IDid of the element that fired the action
$OMC_ACTIONUI_TRIGGER_VIEW_PART_IDPart ID (e.g. column index for Table)
$OMC_ACTIONUI_TRIGGER_CONTEXTJSON string with full trigger context

ActionUI remote bridge (OMC 5.3). An applet with an ActionUI window also serves a JSON-RPC bridge on a Unix socket, so a Python handler can READ window state - which omc_dialog_control has never been able to do.

VariableDescription
$ACTIONUI_REMOTE_ENDPOINTSocket path. $OMC_ACTIONUI_REMOTE_ENDPOINT is the same value.
$ACTIONUI_WINDOW_UUIDThe window. Unprefixed alias of $OMC_ACTIONUI_WINDOW_UUID.
import omc                      # in Contents/Library/Packages, already on PYTHONPATH
win = omc.window()
name = win.get_string(101)      # live, not the dispatch-time snapshot
rows = win.get_rows(5)

Writing still works either way. Shell handlers keep using omc_dialog_control; there is no shell client. See docs/omc_python_bridge_guide.md.

Runtime Tools

All tools are at $OMC_OMC_SUPPORT_PATH/. Source a shared library at the top of every script:

source "${OMC_APP_BUNDLE_PATH}/Contents/Resources/Scripts/lib.myapp.sh"

Set up aliases once in lib.myapp.sh:

dialog_tool="$OMC_OMC_SUPPORT_PATH/omc_dialog_control"
next_cmd="$OMC_OMC_SUPPORT_PATH/omc_next_command"

omc_dialog_control — set control values and state

# Set text value
"$dialog_tool" "$window_uuid" <id> "value"

# ActionUI: set rich content
"$dialog_tool" "$window_uuid" <id> markdown "# Hello"
"$dialog_tool" "$window_uuid" <id> html "<p>Hello</p>"

# Enable / disable a control
"$dialog_tool" "$window_uuid" <id> omc_enable
"$dialog_tool" "$window_uuid" <id> omc_disable

# Show / hide a control
"$dialog_tool" "$window_uuid" <id> omc_show
"$dialog_tool" "$window_uuid" <id> omc_hide

# Set window title
"$dialog_tool" "$window_uuid" omc_window "My Window Title"

# Feed a table from stdin (tab-separated; each line is a row)
printf "Label1\t/data/1\nLabel2\t/data/2\n" | \
  "$dialog_tool" "$window_uuid" <tableID> omc_table_set_rows_from_stdin

# Select a Table/List row programmatically (works for Table and List; fires no actionID)
"$dialog_tool" "$window_uuid" <tableID> omc_select_row 3              # by 0-based index
"$dialog_tool" "$window_uuid" <tableID> omc_select_row_with_content "Report.pdf"  # first row with text in any column
"$dialog_tool" "$window_uuid" <tableID> omc_select_row_with_content "42" 1        # text must be in column 1 (1-based)
"$dialog_tool" "$window_uuid" <tableID> omc_deselect                  # clear selection

# ActionUI only: set a property directly (value is string or JSON fragment)
"$dialog_tool" "$window_uuid" <id> omc_set_property "options" '["A","B","C"]'
"$dialog_tool" "$window_uuid" <id> omc_set_property "disabled" true

# ActionUI only: present an alert
"$dialog_tool" "$window_uuid" omc_window \
  omc_present_alert "Title" "Message" "OK::ok.action" "Cancel:cancel:"

# ActionUI only: insert / remove elements at runtime
"$dialog_tool" "$window_uuid" <parentID> \
  omc_insert_element '{"id":99,"type":"Text","properties":{"text":"Hi"}}'
"$dialog_tool" "$window_uuid" <elementID> omc_remove_element

Button spec for alerts: "title:role:actionID" — role is cancel, destructive, or empty for default.

omc_next_command — chain to another command

"$next_cmd" "$OMC_CURRENT_COMMAND_GUID" "MyApp.next.step"

Schedules MyApp.next.step to run after the current script exits. The chained script runs in a fresh environment with the same window context.

To chain back to the applet's main command, target it by <NAME>.main (e.g. MyApp.main), bare main, or the legacy top! — all three resolve to the main command whether it was declared with no COMMAND_ID or with an explicit <NAME>.main / main id. The same aliases work in NEXT_COMMAND_ID.

Other support tools (full usage in docs/<tool>--help.md)

ToolPurposeTypical call
alertModal alert; choice returned via exit code (0=OK, 1=Cancel)alert --level caution --ok "Go" --cancel "Cancel" "Sure?"
pasteboardCross-script key-value store; prefix keys with app name + window UUIDpasteboard my_key set "v" / pasteboard my_key get
notifymacOS notificationnotify --title "MyApp" "Done."
plisterPlist read/write (for complex edits: plutil -convert json → edit → xml1)plister get value "$plist" /COMMAND_LIST/0/NAME

Hard Rules for Agents — read before writing scripts

Each of these caused a real applet failure for an AI agent. The full explanations, workaround tables, and a debug-logging recipe are in docs/omc_agent_tips_and_troubleshooting.md — read it when any of these bites or when behavior can't be explained from the code.

  1. .sh scripts run under /bin/sh = macOS bash 3.2 in POSIX mode. There is no bash 4/5 on macOS. Process substitution (done < <(cmd)), mapfile, declare -A, and ${var,,} are fatal parse errors that kill the script mid-file with no UI feedback. Validate with sh -n, never bash -nbash -n passes scripts that die under OMC.
  2. Window init code goes in INIT_SUBCOMMAND_ID (runs before the window appears). A non-blocking window's main command script runs at an unpredictable time — keep it exit 0.
  3. Views inside a not-yet-loaded LoadableView can't be targeted by omc_dialog_control. Populate them in their viewDidLoadActionID handler from state files; have init write its readiness file last (atomic mv) and let handlers poll for it.
  4. Never set a Table's value to select a row — a plain value (omc_dialog_control <id> "text") replaces the rows with one string, it does not move the selection. To select programmatically use the dedicated verbs: omc_select_row <0-based index>, omc_select_row_with_content <text> [1-based column] (omit column or 0 = match any column; selects the first match), or omc_deselect to clear. These work on Table and List, fire no actionID, and leave the rows untouched; read the result back via $OMC_ACTIONUI_VIEW_<id>_VALUE. Feed rows via omc_table_set_rows_from_stdin; extra tab-separated fields beyond the declared columns act as hidden columns (read via $OMC_ACTIONUI_TABLE_<ID>_COLUMN_<N>_VALUE).
  5. Pickers deliver (and are set by) the 1-based option INDEX, not the option title; TabView delivers the 0-based tab index as trigger context. Persist each picker's ordered option list to a state file and resolve index → name in handlers. Validate every control-event value before using it — programmatic options/value updates can fire actions with bogus values.
  6. When runtime behavior can't be determined from code, instrument it: add a dbg() logger to /tmp (gated on a flag file), log $OMC_ACTIONUI_TRIGGER_VIEW_ID/_PART_ID/_CONTEXT in every handler, ask the user to perform the UI operation once, read the log back. Don't guess.
  7. Handlers are testable headlessly — do not hand-roll a stub directory. appletbuilder test <App.app> runs Tests/*.test.sh against a mock OMC environment: real handlers, real plister and pasteboard, a stubbed app-modal alert with scripted answers, and a recording omc_dialog_control so you can assert on what a handler pushed toward the window (ui_value, ui_rows, ui_title). Shell and Python applets both. See the Testing an Applet section for the shape and the traps, then read docs/omctest_guide.md for the full API; copy from PackageBuilderApp/Tests/ or NotarizeApp/Tests/ if you have them. What it does NOT cover: rendering and layout, the actionID-to-COMMAND_ID wiring (validate cross-checks that one statically, so read its warnings), and anything the harness cannot intercept — a system binary called by absolute path (/usr/bin/codesign, security), state under $HOME, a background worker the handler spawns, or a global pasteboard key shared with every other test run. Each of those needs an overridable-variable seam in the applet's own lib.

Execution Modes

Set via EXECUTION_MODE in a command dictionary.

ModeDescriptionUse when
exe_script_fileRuns matching script from Scripts/; async; full env varsPrimary mode for all applets — every handler
exe_script_file_with_output_windowLike above; stdout shown in an output windowDebugging; long-running tasks
exe_shell_scriptInline COMMAND string; __SPECIAL_WORDS__ substituted; asyncContextual menu one-liners

Other modes exist for special cases — exe_shell_script_with_output_window, exe_system, exe_applescript[_with_output_window], exe_terminal, exe_iterm — see docs/omc_command_reference.md.

The main command (no COMMAND_ID) attaches ACTIONUI_WINDOW or NIB_DIALOG and opens the window on launch.

Activation Modes

Set via ACTIVATION_MODE. Controls when the command is visible in contextual menus.

ModeActivates when
act_alwaysAlways (use for applet launcher commands and subcommands)
act_fileA file is selected / right-clicked
act_folderA folder is selected / right-clicked
act_file_or_folderEither a file or folder is selected
act_file_or_folder_not_finder_windowFile or folder (not just a Finder background click)
act_selected_textText is selected in any application

Dialog Integration Keys

Attach a dialog to a command by adding one of these keys to the command dict:

ACTIONUI_WINDOW

<key>ACTIONUI_WINDOW</key>
<dict>
    <key>JSON_NAME</key>
    <string>MainWindow</string>          <!-- filename without .json, in Base.lproj/ -->
    <key>INIT_SUBCOMMAND_ID</key>
    <string>MyApp.main.init</string>     <!-- fires when window loads; populate tables here -->
    <key>WINDOW_DID_ACTIVATE_SUBCOMMAND_ID</key>
    <string>MyApp.main.activated</string>
    <key>WINDOW_DID_DEACTIVATE_SUBCOMMAND_ID</key>
    <string>MyApp.main.deactivated</string>
</dict>

The JSON file Contents/Resources/Base.lproj/MainWindow.json defines the UI (see ActionUI skill).

NIB_DIALOG (legacy)

Same shape with NIB_NAME instead of JSON_NAME, plus IS_BLOCKING (false = modeless), END_OK_SUBCOMMAND_ID, END_CANCEL_SUBCOMMAND_ID. See docs/Nib-Guide.md.

INIT_SUBCOMMAND_ID fires when the window opens — use it to populate initial data. END_OK_SUBCOMMAND_ID / END_CANCEL_SUBCOMMAND_ID fire on confirm / cancel (these and IS_BLOCKING work for ACTIONUI_WINDOW too).

Other Useful Command Keys

KeyTypeDescription
NEXT_COMMAND_IDstringStatic: always chains to this command after execution
END_NOTIFICATIONdictShows a completion alert (TITLE, MESSAGE strings)
PROGRESSdictProgress bar dialog (TITLE, MODE: steps/counter/indeterminate)
INPUT_DIALOGdictPrompts for user input before running (TYPE: text/password/popup/combo)
SUBMENU_NAMEstringGroups this command under a submenu in contextual menus
CATEGORIESarrayFilter categories for OMC's command list UI

App Lifetime Event COMMAND_IDs

These are invoked automatically without being declared in COMMAND_LIST:

COMMAND_IDWhen
app.will.launchBefore the app is fully launched
app.did.launchAfter launch completes
app.did.activateApp comes to the foreground
app.did.deactivateApp loses focus
app.will.terminateApp is about to quit — use to clean up background processes

UI Dialogs

OMC applets have two dialog paradigms. Most new applets should use ActionUI JSON.

ActionUI JSON (recommended, OMC 5.0+, macOS 14.6+)

A JSON file in Contents/Resources/Base.lproj/ defines the UI using the ActionUI framework. Connect it to a command via ACTIONUI_WINDOW in Command.plist:

<key>ACTIONUI_WINDOW</key>
<dict>
    <key>JSON_NAME</key><string>MainWindow</string>
    <key>INIT_SUBCOMMAND_ID</key><string>MyApp.main.init</string>
</dict>

For ActionUI JSON format (element types, properties, layout patterns) — read the ActionUI skill. The OMC-specific concerns are:

  • actionID property value on buttons/pickers/etc. is the COMMAND_ID of the handler script that runs on click
  • valueChangeActionID fires as the value changes (e.g., every keystroke in a TextField)
  • Window UUID is $OMC_ACTIONUI_WINDOW_UUID
  • Read element values via $OMC_ACTIONUI_VIEW_<id>_VALUE
  • INIT_SUBCOMMAND_ID fires when the window loads — populate tables, pickers, and initial state here

Init script pattern:

# MyApp.main.init.sh
source "${OMC_APP_BUNDLE_PATH}/Contents/Resources/Scripts/lib.myapp.sh"

# Populate a picker (element id=20)
"$dialog_tool" "$OMC_ACTIONUI_WINDOW_UUID" 20 \
  omc_set_property "options" '["Option A","Option B","Option C"]'

# Enable a button that starts disabled (element id=10)
"$dialog_tool" "$OMC_ACTIONUI_WINDOW_UUID" 10 omc_enable

NIB Dialogs (legacy — do not use for new applets)

Nib (Interface Builder) dialogs predate ActionUI. .nib files can only be edited in Xcode, so agents cannot work on them directly. When maintaining an existing NIB applet: the window UUID is $OMC_NIB_DLG_GUID, control values arrive as $OMC_NIB_DIALOG_CONTROL_<tag>_VALUE, and the dialog attaches via a NIB_DIALOG dict in the command manifest. Full reference: docs/Nib-Guide.md and docs/omc_controls_user_defined_runtime_attributes.md. If the ask is to replace the nib rather than maintain it, that port is an agent-doable job with its own guide: docs/nib_to_actionui_migration.md.

AppletBuilder

AppletBuilder (Distribution/AppletBuilder.app) is the tool for creating and maintaining applets. Humans use its GUI; AI agents drive the same operations from the command line via the bundled appletbuilder CLI — create from a template, validate, prettify/preview ActionUI, and rebuild — running the same underlying code as the GUI.

Agent CLI

Distribution/AppletBuilder.app/Contents/Resources/Agents/appletbuilder <command> [args]

Progress and validation detail go to stderr; capturable results (a created applet's path, prettified JSON, a screenshot path, listings) go to stdout. Exit codes: 0 ok · 2 warnings · 1 errors.

CommandDoes
`create (--template <namepath> | --clone <App.app>) --name --dest [--bundle-id ] [--python] [--icon <name
validate <App.app | Command.json | UI.json | script>Auto-detects the target and runs the matching validator(s). For a bundle: Info.plist + command manifest (Layer 1/2) + every script + every ActionUI JSON.
build <App.app> [--identity <id>] [--thin arm64|x86_64] [--test] [--warnings-as-errors] [--update-python] [--force]Full validation, then refresh framework/executable (newer version auto-copies; --force re-copies even when unchanged), thin, and codesign. Halts before signing on validation errors. Independently, a working embedded Python is left untouched unless --update-python is given; a missing/broken runtime is always installed. Replacing the runtime wipes anything pip-installed into its site-packages — install deps into Contents/Library/Packages (on PYTHONPATH) so they survive. --test runs the applet's test suite after the refresh and before signing, halting on failure.
test <App.app> [--tests <dir>] [--filter <glob>] [--verbose] [--keep-scratch] [--list]Run the applet's Tests/*.test.sh against a mock OMC environment: real handlers, stubbed alert, a recording omc_dialog_control you can assert against. Validates the bundle first. Detail to stderr, omctest: N passed, M failed, K files to stdout. See docs/omctest_guide.md.
prettify <file.json> [--stdout]Reformat JSON in place (or to stdout).
preview <UI.json> [--screenshot <out.png>]Render an ActionUI view to a PNG (read it to inspect the layout); a MainMenu.json menu-bar doc prints a text summary instead. Needs a GUI session.
list-templates / list-iconsNames for --template / --icon.

Example — create a Python applet, then validate and build it:

AB="Distribution/AppletBuilder.app/Contents/Resources/Agents/appletbuilder"
NEWAPP=$("$AB" create --template "ActionUI Window" --name MyApp --dest ~/Desktop --python --icon Bolt)
"$AB" validate "$NEWAPP"
"$AB" build "$NEWAPP" --thin arm64

Full reference: Distribution/AppletBuilder.app/Contents/Resources/Agents/README.md.

Templates

TemplateUse when
EmptyMinimal bundle; no dialog
ActionUI WindowActionUI JSON dialog (recommended for OMC 5.0+)
ActionUI WebActionUI dialog with embedded WebView
Nib WindowNIB (Interface Builder) dialog
Nib WebNIB dialog with embedded WebView

The Name also becomes the executable name and script prefix. New applets are created with a Command.json manifest. Humans can do the same from the GUI's New Applet panel.

For an existing applet

Shortened here. Read the whole file on GitHub.

Signals

GitHub stars
32
Forks
5
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
omc
Source
github.com/abra-code/omc