OMC Skill
SkillDev toolsCreate, 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.
No other account needed.
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
| Key | Type | Notes |
|---|---|---|
NAME | string | Human-readable label. Required. Shared across a command group. |
COMMAND_ID | string | Dot-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_MODE | string | How the command runs. Default: exe_script_file or exe_shell_script if COMMAND is set. |
ACTIVATION_MODE | string | What 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 file | Handles COMMAND_ID |
|---|---|
MyApp.main.sh | the main command (no COMMAND_ID, or COMMAND_ID = MyApp.main) — MyApp is the NAME |
MyApp.results.selected.py | MyApp.results.selected |
MyApp.settings.save.sh | MyApp.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
| Variable | Description |
|---|---|
$OMC_APP_BUNDLE_PATH | Full path to the running applet's .app bundle |
$OMC_APP_PROCESS_ID | PID 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_PATH | Path to OMC's support directory — all runtime tools live here |
$OMC_CURRENT_COMMAND_GUID | GUID of the current command invocation (pass to omc_next_command) |
$OMC_OBJ_PATH | Path of the file/folder that triggered the command (drag & drop / open panel / service) |
Window Context
| Variable | Description |
|---|---|
$OMC_ACTIONUI_WINDOW_UUID | Window UUID for ActionUI dialog scripts |
$OMC_NIB_DLG_GUID | Window UUID for NIB dialog scripts |
Control Values
| Variable | Description |
|---|---|
$OMC_ACTIONUI_VIEW_<N>_VALUE | Current value of ActionUI element with id N |
$OMC_NIB_DIALOG_CONTROL_<N>_VALUE | Value of NIB control with tag N |
$OMC_NIB_TABLE_<N>_COLUMN_<M>_VALUE | Selected 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:
| Variable | Description |
|---|---|
$OMC_ACTIONUI_TRIGGER_VIEW_ID | id of the element that fired the action |
$OMC_ACTIONUI_TRIGGER_VIEW_PART_ID | Part ID (e.g. column index for Table) |
$OMC_ACTIONUI_TRIGGER_CONTEXT | JSON 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.
| Variable | Description |
|---|---|
$ACTIONUI_REMOTE_ENDPOINT | Socket path. $OMC_ACTIONUI_REMOTE_ENDPOINT is the same value. |
$ACTIONUI_WINDOW_UUID | The 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)
| Tool | Purpose | Typical call |
|---|---|---|
alert | Modal alert; choice returned via exit code (0=OK, 1=Cancel) | alert --level caution --ok "Go" --cancel "Cancel" "Sure?" |
pasteboard | Cross-script key-value store; prefix keys with app name + window UUID | pasteboard my_key set "v" / pasteboard my_key get |
notify | macOS notification | notify --title "MyApp" "Done." |
plister | Plist 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.
.shscripts 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 withsh -n, neverbash -n—bash -npasses scripts that die under OMC.- 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 itexit 0. - Views inside a not-yet-loaded
LoadableViewcan't be targeted byomc_dialog_control. Populate them in theirviewDidLoadActionIDhandler from state files; have init write its readiness file last (atomicmv) and let handlers poll for it. - 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 or0= match any column; selects the first match), oromc_deselectto 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 viaomc_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). - 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.
- 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/_CONTEXTin every handler, ask the user to perform the UI operation once, read the log back. Don't guess. - Handlers are testable headlessly — do not hand-roll a stub directory.
appletbuilder test <App.app>runsTests/*.test.shagainst a mock OMC environment: real handlers, realplisterandpasteboard, a stubbed app-modalalertwith scripted answers, and a recordingomc_dialog_controlso 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 readdocs/omctest_guide.mdfor the full API; copy fromPackageBuilderApp/Tests/orNotarizeApp/Tests/if you have them. What it does NOT cover: rendering and layout, theactionID-to-COMMAND_ID wiring (validatecross-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.
| Mode | Description | Use when |
|---|---|---|
exe_script_file | Runs matching script from Scripts/; async; full env vars | Primary mode for all applets — every handler |
exe_script_file_with_output_window | Like above; stdout shown in an output window | Debugging; long-running tasks |
exe_shell_script | Inline COMMAND string; __SPECIAL_WORDS__ substituted; async | Contextual 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.
| Mode | Activates when |
|---|---|
act_always | Always (use for applet launcher commands and subcommands) |
act_file | A file is selected / right-clicked |
act_folder | A folder is selected / right-clicked |
act_file_or_folder | Either a file or folder is selected |
act_file_or_folder_not_finder_window | File or folder (not just a Finder background click) |
act_selected_text | Text 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
| Key | Type | Description |
|---|---|---|
NEXT_COMMAND_ID | string | Static: always chains to this command after execution |
END_NOTIFICATION | dict | Shows a completion alert (TITLE, MESSAGE strings) |
PROGRESS | dict | Progress bar dialog (TITLE, MODE: steps/counter/indeterminate) |
INPUT_DIALOG | dict | Prompts for user input before running (TYPE: text/password/popup/combo) |
SUBMENU_NAME | string | Groups this command under a submenu in contextual menus |
CATEGORIES | array | Filter categories for OMC's command list UI |
App Lifetime Event COMMAND_IDs
These are invoked automatically without being declared in COMMAND_LIST:
| COMMAND_ID | When |
|---|---|
app.will.launch | Before the app is fully launched |
app.did.launch | After launch completes |
app.did.activate | App comes to the foreground |
app.did.deactivate | App loses focus |
app.will.terminate | App 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:
actionIDproperty value on buttons/pickers/etc. is theCOMMAND_IDof the handler script that runs on clickvalueChangeActionIDfires 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_IDfires 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.
| Command | Does |
|---|---|
| `create (--template <name | path> | --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-icons | Names 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
| Template | Use when |
|---|---|
Empty | Minimal bundle; no dialog |
ActionUI Window | ActionUI JSON dialog (recommended for OMC 5.0+) |
ActionUI Web | ActionUI dialog with embedded WebView |
Nib Window | NIB (Interface Builder) dialog |
Nib Web | NIB 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